mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-28 13:28:35 +00:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f7d5a2d1 | ||
|
|
66684e7e23 | ||
|
|
7b8b58f8ca | ||
|
|
71da5d0928 | ||
|
|
2569b244e2 | ||
|
|
09aefa96ad | ||
|
|
fd3efd53be | ||
|
|
6d43d840db | ||
|
|
955ce9b670 | ||
|
|
b57919e2ce | ||
|
|
72f3fb2a2c | ||
|
|
12f383f252 | ||
|
|
01b25b5821 | ||
|
|
c335c2c85a | ||
|
|
1016ec2a14 | ||
|
|
8da2841fdd | ||
|
|
ccda17ede2 | ||
|
|
e18be9b21e | ||
|
|
53ea9ff102 | ||
|
|
cb9df5e171 | ||
|
|
041bcc96a9 | ||
|
|
230cff583f | ||
|
|
b543093bde | ||
|
|
520e1e9c8f | ||
|
|
3a72f4bd30 | ||
|
|
8cca74d5ef | ||
|
|
ce2c6c3165 | ||
|
|
0b21026eef | ||
|
|
3eff9c9779 | ||
|
|
0aca1e8e05 | ||
|
|
9b3f309598 | ||
|
|
5c3a0965bc | ||
|
|
55edbec6fa | ||
|
|
d988c7bd06 | ||
|
|
b8db903320 | ||
|
|
1b2c2515bc | ||
|
|
aa2b64476e | ||
|
|
15d2881bc8 | ||
|
|
f55a131dea | ||
|
|
4036ef8764 | ||
|
|
a7533ff7f6 | ||
|
|
b6f82a656f | ||
|
|
4190ffc654 | ||
|
|
6b5b3afbca | ||
|
|
f337af9e90 | ||
|
|
37741b5b45 | ||
|
|
394b1f499d | ||
|
|
cdbb515379 | ||
|
|
5f376009d0 | ||
|
|
2ac0009b87 | ||
|
|
307e394da4 | ||
|
|
a293ec0db3 | ||
|
|
d4aae4f311 | ||
|
|
4dd58aba78 | ||
|
|
0b20a5d82e | ||
|
|
5039f45be4 | ||
|
|
a755540708 | ||
|
|
81afdcf745 | ||
|
|
f7e165b5ff | ||
|
|
9a58823867 | ||
|
|
bba738f5f4 | ||
|
|
913cea0018 | ||
|
|
6602ce9960 | ||
|
|
f2864ebdf6 | ||
|
|
b93828cd33 | ||
|
|
cd3ca1ee25 | ||
|
|
6491577501 | ||
|
|
5c1043fe88 | ||
|
|
78637823c8 | ||
|
|
ee12ec36cc | ||
|
|
1b1dfa8893 | ||
|
|
a482ee1fe4 | ||
|
|
20daae4de9 | ||
|
|
bb619a64fe | ||
|
|
67954a93b2 | ||
|
|
0b44cebe00 | ||
|
|
21ab8fe89c | ||
|
|
fbbc0af7bc | ||
|
|
1999b7a6c5 | ||
|
|
fa5462d674 | ||
|
|
b3736d6c6c | ||
|
|
031ecb5cb0 | ||
|
|
2faeefb063 | ||
|
|
d384860c34 | ||
|
|
c417b0c15a | ||
|
|
e49a401c23 | ||
|
|
27f51f1234 | ||
|
|
ad0f563816 | ||
|
|
44330a618b | ||
|
|
4d312cbc65 | ||
|
|
6bac561789 | ||
|
|
31254009cc | ||
|
|
b27a55e802 | ||
|
|
cab0e30ceb | ||
|
|
112f96bae0 | ||
|
|
66b85c96f4 | ||
|
|
0513481ec8 | ||
|
|
f62762b4d1 | ||
|
|
27e0ed30fe | ||
|
|
4bbf0a46b5 | ||
|
|
de77894e59 | ||
|
|
5a9615a1f2 | ||
|
|
c3077ee067 | ||
|
|
ea596eb4a2 | ||
|
|
6590d78298 | ||
|
|
378605cd00 | ||
|
|
56e455c745 | ||
|
|
8d70a0e0bc | ||
|
|
b67e9e4aa8 | ||
|
|
44c3ad6810 | ||
|
|
69c05e5843 | ||
|
|
6ab91a5c88 | ||
|
|
759c7f5490 | ||
|
|
03b4a85825 | ||
|
|
737c4809e3 | ||
|
|
3657badb31 | ||
|
|
ef69a46e6a | ||
|
|
78106836be | ||
|
|
a7d1a88519 | ||
|
|
15ac3d8b0b | ||
|
|
08f888a326 | ||
|
|
ee9822fd42 | ||
|
|
17eb2f02f4 | ||
|
|
067419b7cd | ||
|
|
acfee42735 | ||
|
|
62966ac550 | ||
|
|
23c3c3cc0c | ||
|
|
0a65a37eed | ||
|
|
0df1005cb2 | ||
|
|
807b2d5c0a | ||
|
|
f65b28a189 | ||
|
|
a292e52b34 | ||
|
|
4555f8a9a6 | ||
|
|
0fea5564b6 | ||
|
|
476b7f43a2 | ||
|
|
d5d846f481 | ||
|
|
6fd356b654 | ||
|
|
48351d6da8 | ||
|
|
3068dad410 | ||
|
|
61d6838b2c | ||
|
|
e9acacdd5d | ||
|
|
3bb26fde2b | ||
|
|
7b7987363f | ||
|
|
3193ef0a89 | ||
|
|
a62298a635 | ||
|
|
e6c7832485 | ||
|
|
c3073d6e74 | ||
|
|
b34582e6ba | ||
|
|
5136ffd10d | ||
|
|
5988941032 | ||
|
|
c6145a1101 | ||
|
|
dd67b0ee61 | ||
|
|
fb4a75c5aa |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.21.2"
|
||||
__version__ = "15.23.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -105,7 +105,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"
|
||||
@@ -461,7 +461,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 14:04:26.553554",
|
||||
"modified": "2024-03-15 12:11:36.085158",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -490,4 +490,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "2023-03-07 11:02:24.535714",
|
||||
"modified": "2024-04-28 14:40:50.910884",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
@@ -139,4 +140,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -454,7 +454,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)
|
||||
|
||||
@@ -76,6 +76,7 @@ class PaymentEntry(AccountsController):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_liability_account()
|
||||
self.validate_advance_account_currency()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
@@ -158,6 +159,22 @@ class PaymentEntry(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def validate_advance_account_currency(self):
|
||||
if self.book_advance_payments_in_separate_party_account is True:
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
|
||||
)
|
||||
)
|
||||
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
@@ -1114,88 +1131,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"
|
||||
@@ -1210,7 +1210,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")]
|
||||
@@ -1222,8 +1222,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)
|
||||
|
||||
@@ -1440,6 +1440,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();
|
||||
},
|
||||
|
||||
@@ -573,6 +573,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)
|
||||
|
||||
@@ -1102,7 +1102,60 @@ 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")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
@@ -653,7 +654,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,
|
||||
|
||||
@@ -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": "2020-08-03 16:56:45.744905",
|
||||
"modified": "2024-05-03 17:16:51.666461",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "PSOA Cost Center",
|
||||
@@ -27,4 +29,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1091,7 +1091,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]
|
||||
@@ -1182,7 +1182,7 @@ class PurchaseInvoice(BuyingController):
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -2040,7 +2040,7 @@
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact & Address"
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
@@ -2187,7 +2187,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",
|
||||
|
||||
@@ -388,6 +388,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:
|
||||
|
||||
@@ -1766,6 +1766,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",
|
||||
|
||||
@@ -112,11 +112,7 @@ class Subscription(Document):
|
||||
"""
|
||||
_current_invoice_start = None
|
||||
|
||||
if (
|
||||
self.is_new_subscription()
|
||||
and self.trial_period_end
|
||||
and getdate(self.trial_period_end) > getdate(self.start_date)
|
||||
):
|
||||
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
_current_invoice_start = self.trial_period_start
|
||||
@@ -143,7 +139,7 @@ class Subscription(Document):
|
||||
else:
|
||||
billing_cycle_info = self.get_billing_cycle_data()
|
||||
if billing_cycle_info:
|
||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
|
||||
if getdate(self.start_date) < getdate(date):
|
||||
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||
|
||||
# For cases where trial period is for an entire billing interval
|
||||
@@ -234,14 +230,14 @@ class Subscription(Document):
|
||||
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||
self.status = "Past Due Date"
|
||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
||||
elif not self.has_outstanding_invoice():
|
||||
self.status = "Active"
|
||||
|
||||
def is_trialling(self) -> bool:
|
||||
"""
|
||||
Returns `True` if the `Subscription` is in trial period.
|
||||
"""
|
||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
||||
return not self.period_has_passed(self.trial_period_end)
|
||||
|
||||
@staticmethod
|
||||
def period_has_passed(
|
||||
@@ -288,14 +284,6 @@ class Subscription(Document):
|
||||
def invoice_document_type(self) -> str:
|
||||
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
def is_new_subscription(self) -> bool:
|
||||
"""
|
||||
Returns `True` if `Subscription` has never generated an invoice
|
||||
"""
|
||||
return self.is_new() or not frappe.db.exists(
|
||||
{"doctype": self.invoice_document_type, "subscription": self.name}
|
||||
)
|
||||
|
||||
def validate(self) -> None:
|
||||
self.validate_trial_period()
|
||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||
@@ -604,7 +592,7 @@ class Subscription(Document):
|
||||
return False
|
||||
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
||||
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
|
||||
getdate(posting_date) == getdate(self.current_invoice_start)
|
||||
):
|
||||
return True
|
||||
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
@@ -53,7 +53,7 @@
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
@@ -87,7 +87,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-04-13 18:44:25.055382",
|
||||
"modified": "2024-04-30 10:26:48.21829",
|
||||
"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(
|
||||
|
||||
@@ -238,10 +238,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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1094,7 +1098,7 @@ def get_companies():
|
||||
def get_children(doctype, parent, company, is_root=False):
|
||||
from erpnext.accounts.report.financial_statements import sort_accounts
|
||||
|
||||
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
|
||||
parent_fieldname = "parent_" + frappe.scrub(doctype)
|
||||
fields = ["name as value", "is_group as expandable"]
|
||||
filters = [["docstatus", "<", 2]]
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
@@ -408,15 +408,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 +537,15 @@
|
||||
"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
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -589,7 +589,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-01-15 17:35:49.226603",
|
||||
"modified": "2024-04-18 16:45:47.306032",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -633,4 +633,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +92,10 @@ class Asset(AccountsController):
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
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",
|
||||
@@ -354,7 +354,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."
|
||||
)
|
||||
@@ -696,7 +696,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 self.available_for_use_date <= nowdate():
|
||||
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -704,8 +704,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,
|
||||
@@ -719,8 +719,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,
|
||||
@@ -1116,8 +1116,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
|
||||
|
||||
@@ -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)
|
||||
@@ -1698,7 +1698,7 @@ def create_asset(**args):
|
||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||
"number_of_depreciations_booked": args.number_of_depreciations_booked 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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -143,6 +143,10 @@ class AssetCapitalization(StockController):
|
||||
self.make_gl_entries()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
super().on_trash()
|
||||
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
@@ -612,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
|
||||
@@ -649,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"
|
||||
|
||||
@@ -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
|
||||
@@ -599,11 +600,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 +616,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 +640,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,40 +660,9 @@ 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)
|
||||
@@ -734,6 +671,23 @@ def get_straight_line_or_manual_depr_amount(
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
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):
|
||||
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
||||
return (
|
||||
@@ -779,6 +733,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 +743,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 +755,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 +814,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,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr
|
||||
|
||||
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 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
||||
|
||||
def test_daily_prorata_based_depr_on_sl_methond(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)
|
||||
|
||||
@@ -612,6 +612,20 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
return result
|
||||
|
||||
def update_ordered_qty_in_so_for_removed_items(self, removed_items):
|
||||
"""
|
||||
Updates ordered_qty in linked SO when item rows are removed using Update Items
|
||||
"""
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
prev_ordered_qty = frappe.get_cached_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
|
||||
)
|
||||
|
||||
def auto_create_subcontracting_order(self):
|
||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
|
||||
|
||||
@@ -764,12 +764,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
|
||||
|
||||
@@ -800,22 +795,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()
|
||||
|
||||
|
||||
@@ -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-13 11:14:06.516519",
|
||||
"modified": "2024-05-08 18:02:57.342931",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
||||
@@ -3158,6 +3158,9 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
if parent.doctype == "Purchase Order":
|
||||
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
|
||||
|
||||
# need to update ordered qty in Material Request first
|
||||
# bin uses Material Request Items to recalculate & update
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
@@ -787,7 +787,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,
|
||||
|
||||
@@ -205,6 +205,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,
|
||||
}
|
||||
|
||||
@@ -1227,8 +1228,8 @@ def get_accounting_ledger_preview(doc, filters):
|
||||
"debit",
|
||||
"credit",
|
||||
"against",
|
||||
"party",
|
||||
"party_type",
|
||||
"party",
|
||||
"cost_center",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
@@ -1404,7 +1405,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 = {}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -360,4 +360,7 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
# update debit and credit in transaction currency:
|
||||
# if transaction currency is same as account currency,
|
||||
# then debit and credit in transaction currency is same as debit and credit in account currency
|
||||
# else debit and credit divided by exchange rate
|
||||
|
||||
# nosemgrep
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabGL Entry`
|
||||
SET
|
||||
debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate),
|
||||
credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate)
|
||||
WHERE
|
||||
transaction_exchange_rate > 0
|
||||
and transaction_currency is not null
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
cancelled_asset_capitalizations = frappe.get_all(
|
||||
"Asset Capitalization",
|
||||
filters={"docstatus": 2},
|
||||
fields=["name", "target_asset"],
|
||||
)
|
||||
for asset_capitalization in cancelled_asset_capitalizations:
|
||||
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
||||
@@ -0,0 +1,8 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("assets", "doctype", "asset")
|
||||
if frappe.db.has_column("Asset", "purchase_receipt_amount"):
|
||||
rename_field("Asset", "purchase_receipt_amount", "purchase_amount")
|
||||
@@ -373,6 +373,7 @@ erpnext.sales_common = {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
incoming_rate: r.avg_rate,
|
||||
qty:
|
||||
qty /
|
||||
flt(
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact & Address"
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "defaults_tab",
|
||||
@@ -583,7 +583,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-16 19:41:47.971815",
|
||||
"modified": "2024-05-08 18:03:20.716169",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
|
||||
@@ -71,6 +71,8 @@ frappe.ui.form.on("Quotation", {
|
||||
frm.trigger("set_label");
|
||||
frm.trigger("toggle_reqd_lead_customer");
|
||||
frm.trigger("set_dynamic_field_label");
|
||||
frm.set_value("party_name", "");
|
||||
frm.set_value("customer_name", "");
|
||||
},
|
||||
|
||||
set_label: function (frm) {
|
||||
@@ -97,7 +99,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
frappe.dynamic_link = {
|
||||
doc: this.frm.doc,
|
||||
fieldname: "party_name",
|
||||
doctype: doc.quotation_to == "Customer" ? "Customer" : "Lead",
|
||||
doctype: doc.quotation_to,
|
||||
};
|
||||
|
||||
var me = this;
|
||||
@@ -197,6 +199,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
};
|
||||
} else if (this.frm.doc.quotation_to == "Prospect") {
|
||||
this.frm.set_df_property("party_name", "label", "Prospect");
|
||||
this.frm.fields_dict.party_name.get_query = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ class Company(NestedSet):
|
||||
self.validate_abbr()
|
||||
self.validate_default_accounts()
|
||||
self.validate_currency()
|
||||
self.validate_advance_account_currency()
|
||||
self.validate_coa_input()
|
||||
self.validate_perpetual_inventory()
|
||||
self.validate_provisional_account_for_non_stock_items()
|
||||
@@ -192,6 +193,29 @@ class Company(NestedSet):
|
||||
).format(frappe.bold(account[0]))
|
||||
frappe.throw(error_message)
|
||||
|
||||
def validate_advance_account_currency(self):
|
||||
if (
|
||||
self.default_advance_received_account
|
||||
and frappe.get_cached_value("Account", self.default_advance_received_account, "account_currency")
|
||||
!= self.default_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' should be in company currency {1}.").format(
|
||||
frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.default_advance_paid_account
|
||||
and frappe.get_cached_value("Account", self.default_advance_paid_account, "account_currency")
|
||||
!= self.default_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' should be in company currency {1}.").format(
|
||||
frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_currency(self):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
@@ -238,7 +238,7 @@ def update_qty(bin_name, args):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
# actual qty is not up to date in case of backdated transaction
|
||||
if future_sle_exists(args):
|
||||
if future_sle_exists(args, allow_force_reposting=False):
|
||||
last_sle_qty = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.qty_after_transaction)
|
||||
|
||||
@@ -1066,7 +1066,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_delivery_trip(source_name, target_doc=None):
|
||||
def make_delivery_trip(source_name, target_doc=None, kwargs=None):
|
||||
def update_stop_details(source_doc, target_doc, source_parent):
|
||||
target_doc.customer = source_parent.customer
|
||||
target_doc.address = source_parent.shipping_address_name
|
||||
|
||||
@@ -15,6 +15,9 @@ frappe.ui.form.on("Item", {
|
||||
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
|
||||
|
||||
frm.make_methods = {
|
||||
Quotation: () => {
|
||||
open_form(frm, "Quotation", "Quotation Item", "items");
|
||||
},
|
||||
"Sales Order": () => {
|
||||
open_form(frm, "Sales Order", "Sales Order Item", "items");
|
||||
},
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
"section_break_11",
|
||||
"description",
|
||||
"brand",
|
||||
"unit_of_measure_conversion",
|
||||
"uoms",
|
||||
"dashboard_tab",
|
||||
"inventory_section",
|
||||
"inventory_settings_section",
|
||||
@@ -52,8 +54,6 @@
|
||||
"barcodes",
|
||||
"reorder_section",
|
||||
"reorder_levels",
|
||||
"unit_of_measure_conversion",
|
||||
"uoms",
|
||||
"serial_nos_and_batches",
|
||||
"has_batch_no",
|
||||
"create_new_batch",
|
||||
@@ -891,7 +891,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-01-08 18:09:30.225085",
|
||||
"modified": "2024-04-30 13:46:39.098753",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
@@ -964,4 +964,4 @@
|
||||
"states": [],
|
||||
"title_field": "item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +65,13 @@ class Item(Document):
|
||||
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
|
||||
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
|
||||
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
|
||||
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import (
|
||||
ItemVariantAttribute,
|
||||
)
|
||||
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ItemVariantAttribute
|
||||
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
|
||||
|
||||
allow_alternative_item: DF.Check
|
||||
allow_negative_stock: DF.Check
|
||||
asset_category: DF.Link | None
|
||||
asset_naming_series: DF.Literal
|
||||
asset_naming_series: DF.Literal[None]
|
||||
attributes: DF.Table[ItemVariantAttribute]
|
||||
auto_create_assets: DF.Check
|
||||
barcodes: DF.Table[ItemBarcode]
|
||||
|
||||
@@ -52,10 +52,13 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM"
|
||||
"options": "UOM",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -220,7 +223,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 14:02:19.304854",
|
||||
"modified": "2024-04-02 22:18:00.450641",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Price",
|
||||
|
||||
@@ -38,7 +38,7 @@ class ItemPrice(Document):
|
||||
reference: DF.Data | None
|
||||
selling: DF.Check
|
||||
supplier: DF.Link | None
|
||||
uom: DF.Link | None
|
||||
uom: DF.Link
|
||||
valid_from: DF.Date | None
|
||||
valid_upto: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -946,6 +946,128 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
def test_do_not_validate_against_landed_cost_voucher_for_serial_for_legacy_pr(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
sn_item = "Test Don't Validate Against LCV For Serial NO for Legacy PR"
|
||||
sn_item_doc = make_item(
|
||||
sn_item,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-ALCVTDVLCVSNO-.####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
serial_nos = [
|
||||
"SN-ALCVTDVLCVSNO-0001",
|
||||
"SN-ALCVTDVLCVSNO-0002",
|
||||
"SN-ALCVTDVLCVSNO-0003",
|
||||
"SN-ALCVTDVLCVSNO-0004",
|
||||
"SN-ALCVTDVLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
)
|
||||
|
||||
pr.reload()
|
||||
|
||||
for sn in serial_nos:
|
||||
sn_doc = frappe.get_doc("Serial No", sn)
|
||||
sn_doc.db_set(
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"status": "Active",
|
||||
}
|
||||
)
|
||||
|
||||
for row in pr.items:
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name})
|
||||
for sle in stock_ledger_entries:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
|
||||
dn = create_delivery_note(
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
item_code=sn_item,
|
||||
qty=5,
|
||||
rate=100,
|
||||
uom=sn_item_doc.stock_uom,
|
||||
stock_uom=sn_item_doc.stock_uom,
|
||||
)
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": dn.name})
|
||||
for sle in stock_ledger_entries:
|
||||
doc = frappe.get_doc("Stock Ledger Entry", sle.name)
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=20,
|
||||
distribute_charges_based_on="Qty",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
lcv.get_items_from_purchase_receipts()
|
||||
lcv.save()
|
||||
lcv.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 104)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
lcv.cancel()
|
||||
pr.reload()
|
||||
|
||||
for row in pr.items:
|
||||
self.assertEqual(row.valuation_rate, 100)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
row.valuation_rate,
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "avg_rate"),
|
||||
)
|
||||
|
||||
|
||||
def make_landed_cost_voucher(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -790,7 +790,7 @@ def get_available_item_locations(
|
||||
locations = get_locations_based_on_required_qty(locations, required_qty)
|
||||
|
||||
if not ignore_validation:
|
||||
validate_picked_materials(item_code, required_qty, locations)
|
||||
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
|
||||
|
||||
return locations
|
||||
|
||||
@@ -810,7 +810,7 @@ def get_locations_based_on_required_qty(locations, required_qty):
|
||||
return filtered_locations
|
||||
|
||||
|
||||
def validate_picked_materials(item_code, required_qty, locations):
|
||||
def validate_picked_materials(item_code, required_qty, locations, picked_item_details=None):
|
||||
for location in list(locations):
|
||||
if location["qty"] < 0:
|
||||
locations.remove(location)
|
||||
@@ -819,15 +819,25 @@ def validate_picked_materials(item_code, required_qty, locations):
|
||||
remaining_qty = required_qty - total_qty_available
|
||||
|
||||
if remaining_qty > 0:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
if picked_item_details:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is not available in any of the warehouses.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
|
||||
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
||||
filterd_locations = []
|
||||
for row in locations:
|
||||
key = row.warehouse
|
||||
if row.batch_no:
|
||||
@@ -835,6 +845,7 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
||||
|
||||
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
||||
if not picked_qty:
|
||||
filterd_locations.append(row)
|
||||
continue
|
||||
if picked_qty > row.qty:
|
||||
row.qty = 0
|
||||
@@ -845,7 +856,10 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
||||
if row.serial_nos:
|
||||
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
|
||||
|
||||
return locations
|
||||
if row.qty > 0:
|
||||
filterd_locations.append(row)
|
||||
|
||||
return filterd_locations
|
||||
|
||||
|
||||
def get_available_item_locations_for_serial_and_batched_item(
|
||||
|
||||
@@ -977,3 +977,157 @@ class TestPickList(FrappeTestCase):
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
self.assertFalse(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_validation_for_multiple_batches_and_sales_order(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Batch Pick List Item For Multiple Batches",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=6, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 5.0)
|
||||
self.assertEqual(pl1.locations[1].qty, 1.0)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_for_multiple_sales_order_with_multiple_batches(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Batch Pick List Item For Multiple Batches and Sales Order",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Serial No Pick List Item For Multiple Batches and Sales Order",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 10)
|
||||
|
||||
serial_nos = pl1.locations[0].serial_no.split("\n")
|
||||
self.assertEqual(len(serial_nos), 10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
new_serial_nos = pl.locations[0].serial_no.split("\n")
|
||||
self.assertEqual(len(new_serial_nos), 110)
|
||||
|
||||
for sn in serial_nos:
|
||||
self.assertFalse(sn in new_serial_nos)
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Non Serialized Pick List Item For Multiple Batches and Sales Order",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl1 = create_pick_list(so.name)
|
||||
pl1.save()
|
||||
self.assertEqual(pl1.locations[0].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
pl1.submit()
|
||||
|
||||
pl.reload()
|
||||
pl.submit()
|
||||
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||
self.assertTrue(hasattr(pl, "locations"))
|
||||
|
||||
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
|
||||
self.assertEqual(pl.locations[0].qty, 80.0)
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "quantity_section",
|
||||
@@ -240,7 +241,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 16:12:16.257951",
|
||||
"modified": "2024-05-07 15:32:42.905446",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
@@ -251,4 +252,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,7 +858,7 @@ class PurchaseReceipt(BuyingController):
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1163,7 +1163,12 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
qty = item_row.qty
|
||||
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
|
||||
qty = item_row.received_qty
|
||||
|
||||
pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
|
||||
|
||||
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
|
||||
return pending_qty, 0
|
||||
|
||||
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
|
||||
if returned_qty:
|
||||
if returned_qty >= pending_qty:
|
||||
@@ -1172,6 +1177,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
else:
|
||||
pending_qty -= returned_qty
|
||||
returned_qty = 0
|
||||
|
||||
return pending_qty, returned_qty
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
|
||||
@@ -895,6 +895,8 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
create_purchase_order,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
|
||||
po = create_purchase_order()
|
||||
pr = create_pr_against_po(po.name)
|
||||
|
||||
@@ -914,6 +916,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
po.cancel()
|
||||
|
||||
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
|
||||
pr1.append(
|
||||
"items",
|
||||
@@ -2783,6 +2786,84 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
def test_purchase_receipt_bill_for_rejected_quantity_in_purchase_invoice(self):
|
||||
item_code = make_item(
|
||||
"_Test Purchase Receipt Bill For Rejected Quantity",
|
||||
properties={"is_stock_item": 1},
|
||||
).name
|
||||
|
||||
pr = make_purchase_receipt(item_code=item_code, qty=5, rate=100)
|
||||
|
||||
return_pr = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
is_return=1,
|
||||
return_against=pr.name,
|
||||
qty=-2,
|
||||
do_not_submit=1,
|
||||
)
|
||||
return_pr.items[0].purchase_receipt_item = pr.items[0].name
|
||||
return_pr.submit()
|
||||
old_value = frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
self.assertEqual(pi.items[0].qty, 3)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1)
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
pi.submit()
|
||||
self.assertEqual(pi.items[0].qty, 5)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", old_value
|
||||
)
|
||||
|
||||
def test_zero_valuation_rate_for_batched_item(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item = make_item(
|
||||
"_Test Zero Valuation Rate For the Batch Item",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TZVRFORBATCH.#####",
|
||||
"valuation_rate": 200,
|
||||
},
|
||||
)
|
||||
|
||||
pi = make_purchase_receipt(
|
||||
qty=10,
|
||||
rate=0,
|
||||
item_code=item.name,
|
||||
)
|
||||
|
||||
pi.reload()
|
||||
batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle)
|
||||
|
||||
se = make_stock_entry(
|
||||
purpose="Material Issue",
|
||||
item_code=item.name,
|
||||
source=pi.items[0].warehouse,
|
||||
qty=10,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=0,
|
||||
)
|
||||
|
||||
se.submit()
|
||||
|
||||
se.reload()
|
||||
|
||||
self.assertEqual(se.items[0].valuation_rate, 0)
|
||||
self.assertEqual(se.items[0].basic_rate, 0)
|
||||
|
||||
sabb_doc = frappe.get_doc("Serial and Batch Bundle", se.items[0].serial_and_batch_bundle)
|
||||
for row in sabb_doc.entries:
|
||||
self.assertEqual(row.incoming_rate, 0)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -249,8 +249,7 @@ class SerialandBatchBundle(Document):
|
||||
if self.has_serial_no:
|
||||
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
||||
else:
|
||||
if sn_obj.batch_avg_rate.get(d.batch_no):
|
||||
d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
|
||||
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
|
||||
|
||||
available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty"))
|
||||
if self.docstatus == 1:
|
||||
@@ -429,6 +428,9 @@ class SerialandBatchBundle(Document):
|
||||
self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.")
|
||||
|
||||
def check_future_entries_exists(self):
|
||||
if self.flags and self.flags.via_landed_cost_voucher:
|
||||
return
|
||||
|
||||
if not self.has_serial_no:
|
||||
return
|
||||
|
||||
@@ -1149,7 +1151,18 @@ def make_batch_nos(item_code, batch_nos):
|
||||
continue
|
||||
|
||||
batch_nos_details.append(
|
||||
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
|
||||
(
|
||||
batch_no,
|
||||
batch_no,
|
||||
now(),
|
||||
now(),
|
||||
user,
|
||||
user,
|
||||
item.item_code,
|
||||
item.item_name,
|
||||
item.description,
|
||||
1,
|
||||
)
|
||||
)
|
||||
|
||||
fields = [
|
||||
@@ -1162,6 +1175,7 @@ def make_batch_nos(item_code, batch_nos):
|
||||
"item",
|
||||
"item_name",
|
||||
"description",
|
||||
"use_batchwise_valuation",
|
||||
]
|
||||
|
||||
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
|
||||
|
||||
@@ -498,6 +498,8 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
|
||||
make_batch_nos(item_code, batch_nos)
|
||||
self.assertTrue(frappe.db.exists("Batch", batch_id))
|
||||
use_batchwise_valuation = frappe.db.get_value("Batch", batch_id, "use_batchwise_valuation")
|
||||
self.assertEqual(use_batchwise_valuation, 1)
|
||||
|
||||
batch_id = "TEST-BATTCCH-VAL-00001"
|
||||
batch_nos = [{"batch_no": batch_id, "qty": 1}]
|
||||
|
||||
@@ -1340,6 +1340,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
basic_rate: r.avg_rate,
|
||||
qty:
|
||||
Math.abs(r.total_qty) /
|
||||
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"end_time",
|
||||
"limits_dont_apply_on",
|
||||
"item_based_reposting",
|
||||
"do_reposting_for_each_stock_transaction",
|
||||
"errors_notification_section",
|
||||
"notify_reposting_error_to_role"
|
||||
],
|
||||
@@ -65,12 +66,18 @@
|
||||
"fieldname": "errors_notification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Errors Notification"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "do_reposting_for_each_stock_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do reposting for each Stock Transaction"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-01 16:14:29.080697",
|
||||
"modified": "2024-04-24 12:19:40.204888",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
@@ -91,4 +98,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class StockRepostingSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
do_reposting_for_each_stock_transaction: DF.Check
|
||||
end_time: DF.Time | None
|
||||
item_based_reposting: DF.Check
|
||||
limit_reposting_timeslot: DF.Check
|
||||
@@ -29,6 +30,10 @@ class StockRepostingSettings(Document):
|
||||
def validate(self):
|
||||
self.set_minimum_reposting_time_slot()
|
||||
|
||||
def before_save(self):
|
||||
if self.do_reposting_for_each_stock_transaction:
|
||||
self.item_based_reposting = 1
|
||||
|
||||
def set_minimum_reposting_time_slot(self):
|
||||
"""Ensure that timeslot for reposting is at least 12 hours."""
|
||||
if not self.limit_reposting_timeslot:
|
||||
|
||||
@@ -38,3 +38,51 @@ class TestStockRepostingSettings(unittest.TestCase):
|
||||
|
||||
users = get_recipients()
|
||||
self.assertTrue(user in users)
|
||||
|
||||
def test_do_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertTrue(riv)
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
|
||||
def test_do_not_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertFalse(riv)
|
||||
|
||||
@@ -418,7 +418,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.enable_stock_reservation",
|
||||
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Receipt for Sales Order.",
|
||||
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Request for Sales Order.",
|
||||
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Reserve Stock for Sales Order on Purchase"
|
||||
@@ -469,4 +469,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,7 +838,12 @@ def insert_item_price(args):
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price",
|
||||
{"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency},
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"price_list": args.price_list,
|
||||
"currency": args.currency,
|
||||
"uom": args.stock_uom,
|
||||
},
|
||||
["name", "price_list_rate"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -40,16 +40,25 @@ frappe.query_reports["Batch-Wise Balance History"] = {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse_type",
|
||||
label: __("Warehouse Type"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse Type",
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query: function () {
|
||||
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -30,8 +30,15 @@ def execute(filters=None):
|
||||
|
||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||
|
||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||
if (
|
||||
sle_count > SLE_COUNT_LIMIT
|
||||
and not filters.get("item_code")
|
||||
and not filters.get("warehouse")
|
||||
and not filters.get("warehouse_type")
|
||||
):
|
||||
frappe.throw(
|
||||
_("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.")
|
||||
)
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
@@ -121,6 +128,16 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
)
|
||||
|
||||
query = apply_warehouse_filter(query, sle, filters)
|
||||
if filters.warehouse_type and not filters.warehouse:
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
query = query.where(sle.warehouse.isin(warehouses))
|
||||
|
||||
for field in ["item_code", "batch_no", "company"]:
|
||||
if filters.get(field):
|
||||
query = query.where(sle[field] == filters.get(field))
|
||||
@@ -154,6 +171,16 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
||||
)
|
||||
|
||||
query = apply_warehouse_filter(query, sle, filters)
|
||||
if filters.warehouse_type and not filters.warehouse:
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
query = query.where(sle.warehouse.isin(warehouses))
|
||||
|
||||
for field in ["item_code", "batch_no", "company"]:
|
||||
if filters.get(field):
|
||||
if field == "batch_no":
|
||||
|
||||
@@ -220,7 +220,7 @@ def get_serial_nos(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_batch_nos(doctype, txt, searchfield, start, page_len, filters):
|
||||
query_filters = {}
|
||||
|
||||
if txt:
|
||||
if filters.get("voucher_no") and txt:
|
||||
query_filters["batch_no"] = ["like", f"%{txt}%"]
|
||||
|
||||
if filters.get("voucher_no"):
|
||||
@@ -239,5 +239,8 @@ def get_batch_nos(doctype, txt, searchfield, start, page_len, filters):
|
||||
)
|
||||
|
||||
else:
|
||||
if txt:
|
||||
query_filters["name"] = ["like", f"%{txt}%"]
|
||||
|
||||
query_filters["item"] = filters.get("item_code")
|
||||
return frappe.get_all("Batch", filters=query_filters, as_list=True)
|
||||
|
||||
@@ -18,15 +18,24 @@ frappe.query_reports["Stock Ageing"] = {
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse_type",
|
||||
label: __("Warehouse Type"),
|
||||
fieldtype: "Link",
|
||||
width: "80",
|
||||
options: "Warehouse Type",
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query: () => {
|
||||
const company = frappe.query_report.get_filter_value("company");
|
||||
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -434,6 +434,15 @@ class FIFOSlots:
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
sle_query = self.__get_warehouse_conditions(sle, sle_query)
|
||||
elif self.filters.get("warehouse_type"):
|
||||
warehouses = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
||||
|
||||
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ class StockBalanceReport:
|
||||
if self.filters.get("show_stock_ageing_data"):
|
||||
self.sle_entries = self.sle_query.run(as_dict=True)
|
||||
|
||||
# HACK: This is required to avoid causing db query in flt
|
||||
_system_settings = frappe.get_cached_doc("System Settings")
|
||||
with frappe.db.unbuffered_cursor():
|
||||
if not self.filters.get("show_stock_ageing_data"):
|
||||
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
|
||||
|
||||
@@ -231,13 +231,6 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
"convertible": "qty",
|
||||
},
|
||||
{
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
|
||||
@@ -56,13 +56,14 @@ def execute(filters=None):
|
||||
item_value.setdefault((item, item_map[item]["item_group"]), [])
|
||||
item_value[(item, item_map[item]["item_group"])].append(total_stock_value)
|
||||
|
||||
itemwise_brand = frappe._dict(get_itemwise_brand(items))
|
||||
# sum bal_qty by item
|
||||
for (item, item_group), wh_balance in item_balance.items():
|
||||
if not item_ageing.get(item):
|
||||
continue
|
||||
|
||||
total_stock_value = sum(item_value[(item, item_group)])
|
||||
row = [item, item_map[item]["item_name"], item_group, total_stock_value]
|
||||
row = [item, item_map[item]["item_name"], item_group, itemwise_brand.get(item), total_stock_value]
|
||||
|
||||
fifo_queue = item_ageing[item]["fifo_queue"]
|
||||
average_age = 0.00
|
||||
@@ -85,6 +86,10 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_itemwise_brand(items):
|
||||
return frappe.get_all("Item", filters={"name": ("in", items)}, fields=["name", "brand"], as_list=1)
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
"""return columns"""
|
||||
|
||||
@@ -92,6 +97,7 @@ def get_columns(filters):
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + ":Link/Item:150",
|
||||
_("Item Group") + "::120",
|
||||
_("Brand") + ":Link/Brand:120",
|
||||
_("Value") + ":Currency:120",
|
||||
_("Age") + ":Float:120",
|
||||
]
|
||||
|
||||
@@ -553,7 +553,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
self.set_stock_value_difference()
|
||||
|
||||
def get_batch_no_ledgers(self) -> list[dict]:
|
||||
if not self.batchwise_valuation_batches:
|
||||
if not self.batches:
|
||||
return []
|
||||
|
||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
@@ -575,7 +575,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
Sum(child.qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(child.batch_no.isin(self.batchwise_valuation_batches))
|
||||
(child.batch_no.isin(self.batches))
|
||||
& (parent.warehouse == self.sle.warehouse)
|
||||
& (parent.item_code == self.sle.item_code)
|
||||
& (parent.docstatus == 1)
|
||||
@@ -840,6 +840,9 @@ class SerialBatchCreation:
|
||||
self.set_auto_serial_batch_entries_for_inward()
|
||||
self.add_serial_nos_for_batch_item()
|
||||
|
||||
if hasattr(self, "via_landed_cost_voucher") and self.via_landed_cost_voucher:
|
||||
doc.flags.via_landed_cost_voucher = self.via_landed_cost_voucher
|
||||
|
||||
self.set_serial_batch_entries(doc)
|
||||
if not doc.get("entries"):
|
||||
return frappe._dict({})
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Married,既婚,
|
||||
|
@@ -67,3 +67,9 @@ typing-modules = ["frappe.types.DF"]
|
||||
quote-style = "double"
|
||||
indent-style = "tab"
|
||||
docstring-code-format = true
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://erpnext.com/"
|
||||
Repository = "https://github.com/frappe/erpnext.git"
|
||||
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
||||
|
||||
Reference in New Issue
Block a user