mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-04 10:14:39 +00:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34d3f58149 | ||
|
|
efd34817be | ||
|
|
124ad729ab | ||
|
|
c388b7579c | ||
|
|
eaa297475f | ||
|
|
24681bd64f | ||
|
|
b0c742b64e | ||
|
|
18282b2632 | ||
|
|
46115d6f71 | ||
|
|
f62905f7a7 | ||
|
|
88facb7523 | ||
|
|
512877ab46 | ||
|
|
c7e6b2356f | ||
|
|
8e2bfc6bcb | ||
|
|
98260b990c | ||
|
|
3be0f00b3b | ||
|
|
f44478db78 | ||
|
|
db8cb70bd8 | ||
|
|
f26b22ee7f | ||
|
|
1e4807f16d | ||
|
|
698d5be840 | ||
|
|
59e46e22d3 | ||
|
|
14b4d147a8 | ||
|
|
2bed1c8336 | ||
|
|
9e3a04136c | ||
|
|
dcc53cea55 | ||
|
|
430d1e8b2e | ||
|
|
36a366d962 | ||
|
|
dda35b8e51 | ||
|
|
4e0d7d88ec | ||
|
|
36b951d018 | ||
|
|
233e0d8049 | ||
|
|
5f467be0c8 | ||
|
|
70d117e858 | ||
|
|
2fe9fa7ef7 | ||
|
|
b4ca51ad4c | ||
|
|
fb5777cf10 | ||
|
|
06c32993ea | ||
|
|
466c6ee3d4 | ||
|
|
a8831351e3 | ||
|
|
d1f9444be7 | ||
|
|
ceaea16d84 | ||
|
|
119f2e7074 | ||
|
|
9cadb89678 | ||
|
|
da6affbba7 | ||
|
|
6293b08540 | ||
|
|
576ce7e882 | ||
|
|
67d7ffa430 | ||
|
|
d90c8a9150 | ||
|
|
15bb1fdb24 | ||
|
|
509c5c4d17 | ||
|
|
cb028b8740 | ||
|
|
c3447c030a | ||
|
|
1d65f995e7 | ||
|
|
3a6d8cd9a1 | ||
|
|
88e664b79f | ||
|
|
090ee7f042 | ||
|
|
b311b6eb7f | ||
|
|
df64d2ef4e | ||
|
|
690a939572 | ||
|
|
a3639b055c | ||
|
|
331ecc1964 | ||
|
|
736b125e14 | ||
|
|
b9b08c35ef | ||
|
|
7e8d2dc0fc | ||
|
|
39af6bd143 | ||
|
|
300f1a860b | ||
|
|
1441ef2532 | ||
|
|
64fd5c0ba9 | ||
|
|
16fe53b8c7 | ||
|
|
af3089b96e | ||
|
|
3cb29fc035 | ||
|
|
8a93057844 | ||
|
|
805549f793 | ||
|
|
6e497f73f1 | ||
|
|
33f090a8c8 | ||
|
|
20b43b4d93 | ||
|
|
5c1f6f0107 | ||
|
|
6f0c67a242 | ||
|
|
609bfaefba | ||
|
|
2546589b4c | ||
|
|
f311a0fc1c | ||
|
|
a1585b3c53 | ||
|
|
05c0bf5d99 | ||
|
|
247a006b5a | ||
|
|
e0cf6393ee | ||
|
|
1f54c272ac | ||
|
|
9a1321ab02 | ||
|
|
6ca1f9bc73 | ||
|
|
b404f00ece | ||
|
|
0f56fbcf6d | ||
|
|
b3b7e62a90 | ||
|
|
35a728d22b | ||
|
|
ac25d3e1c4 | ||
|
|
90b5f0b7bf | ||
|
|
557a05b0ad | ||
|
|
b41ee667b9 | ||
|
|
fbf6d8c9e2 | ||
|
|
167069b823 | ||
|
|
2d72a37d0c | ||
|
|
77ad224cf6 | ||
|
|
b5b55fad6c | ||
|
|
7cde990d69 | ||
|
|
cedf577b4c | ||
|
|
8e02dcfcaa | ||
|
|
399ed331e3 | ||
|
|
59236bc5bf | ||
|
|
1089cdf213 | ||
|
|
df4f4d9a31 | ||
|
|
f8bbcab3a5 | ||
|
|
369a692af9 | ||
|
|
8cc92e9ca0 | ||
|
|
eebf6cf877 | ||
|
|
ca94ad3a24 | ||
|
|
1a8bf0cf3d | ||
|
|
811953b9b0 | ||
|
|
69a8e0dfac |
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Auto-label PRs based on title"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-label-if-prefix-matches:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title and add label if it matches prefixes
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title.toLowerCase();
|
||||
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
|
||||
|
||||
// Check if the PR title starts with any of the prefixes
|
||||
if (prefixes.some(prefix => title.startsWith(prefix))) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ['skip-release-notes']
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.83.1"
|
||||
__version__ = "14.85.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.db.get_value("Account", name, "company", as_dict=True)
|
||||
if not account:
|
||||
return
|
||||
@@ -420,7 +421,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
if old_name and not from_descendant:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
@@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -514,3 +516,27 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationary": {},
|
||||
"Print and Stationery": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
@@ -46,9 +46,6 @@ class BankClearance(Document):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if self.bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -70,7 +67,6 @@ class BankClearance(Document):
|
||||
"account": self.account,
|
||||
"from": self.from_date,
|
||||
"to": self.to_date,
|
||||
"bank_account": self.bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -93,7 +89,7 @@ class BankClearance(Document):
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.where(loan_disbursement.disbursement_account == self.account)
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
)
|
||||
@@ -121,7 +117,7 @@ class BankClearance(Document):
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
.where(loan_repayment.payment_account == self.account)
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
|
||||
@@ -433,8 +433,22 @@ class JournalEntry(AccountsController):
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
|
||||
customer_details = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Customer Credit Limit",
|
||||
filters={
|
||||
"parent": ["in", customers],
|
||||
"parenttype": ["=", "Customer"],
|
||||
"company": ["=", self.company],
|
||||
},
|
||||
fields=["parent", "bypass_credit_limit_check"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for customer in customers:
|
||||
check_credit_limit(customer, self.company)
|
||||
ignore_outstanding_sales_order = bool(customer_details.get(customer))
|
||||
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
|
||||
|
||||
def validate_cheque_info(self):
|
||||
if self.voucher_type in ["Bank Entry"]:
|
||||
|
||||
@@ -1519,7 +1519,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
elif self.party_type in ("Supplier", "Customer"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -1623,6 +1623,5 @@
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -1852,7 +1852,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
@@ -1984,6 +1984,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, dr_note.save)
|
||||
|
||||
def test_apply_discount_on_grand_total(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
the grand total is calculated correctly without any rounding errors
|
||||
"""
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 21.39,
|
||||
},
|
||||
)
|
||||
invoice.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 15.5,
|
||||
},
|
||||
)
|
||||
|
||||
# the grand total here will be 255.71
|
||||
invoice.disable_rounded_total = 1
|
||||
# apply discount on grand total to adjust the grand total to 255
|
||||
invoice.discount_amount = 0.71
|
||||
invoice.save()
|
||||
|
||||
# check if grand total is 496 and not something like 254.99 due to rounding errors
|
||||
self.assertEqual(invoice.grand_total, 255)
|
||||
|
||||
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
where the tax is calculated on previous row total, the grand total is calculated correctly
|
||||
"""
|
||||
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.extend(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"tax_amount": 100,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# the total here will be 340, so applying 40 discount
|
||||
invoice.discount_amount = 40
|
||||
invoice.save()
|
||||
|
||||
self.assertEqual(invoice.grand_total, 300)
|
||||
|
||||
|
||||
def check_gl_entries(
|
||||
doc,
|
||||
|
||||
@@ -6,6 +6,8 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -77,6 +79,9 @@ class RepostAccountingLedger(Document):
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
warehouse_account_map = get_warehouse_account_map(doc.company)
|
||||
gle_map = doc.get_gl_entries(warehouse_account_map)
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
@@ -155,6 +160,14 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
|
||||
@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -202,9 +204,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_06_repost_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
another_provisional_account = create_account(
|
||||
account_name="Another Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
|
||||
test_cc = company.cost_center
|
||||
default_expense_account = company.default_expense_account
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# change the provisional account
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item",
|
||||
pr.items[0].name,
|
||||
"provisional_expense_account",
|
||||
another_provisional_account,
|
||||
)
|
||||
|
||||
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_doc.company = self.company
|
||||
repost_doc.delete_cancelled_entries = True
|
||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
repost_doc.cancel()
|
||||
repost_doc.delete()
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.default_provisional_account = None
|
||||
company.save()
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
allowed_types = [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Receipt",
|
||||
]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
|
||||
@@ -91,8 +91,8 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
|
||||
@@ -416,9 +416,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1500.01)
|
||||
self.assertEqual(si.grand_total, 1500.01)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.base_grand_total, 1500)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.rounding_adjustment, 0)
|
||||
|
||||
def test_discount_amount_gl_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
@@ -1773,17 +1773,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
@@ -3871,6 +3860,35 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_create_return_invoice_for_self_update(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
invoice = create_sales_invoice()
|
||||
|
||||
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
|
||||
payment_entry.reference_no = "test001"
|
||||
payment_entry.reference_date = getdate()
|
||||
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
r_invoice = make_return_doc(invoice.doctype, invoice.name)
|
||||
|
||||
r_invoice.update_outstanding_for_self = 0
|
||||
r_invoice.save()
|
||||
|
||||
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
|
||||
|
||||
r_invoice.submit()
|
||||
|
||||
self.assertNotEqual(r_invoice.outstanding_amount, 0)
|
||||
|
||||
invoice.reload()
|
||||
|
||||
self.assertEqual(invoice.outstanding_amount, 0)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
"label": "Voucher Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -36,7 +34,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-13 13:40:41.479208",
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
|
||||
@@ -498,7 +498,7 @@ class ReceivablePayableReport:
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
**args,
|
||||
)
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
@@ -111,7 +112,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
def test_accounts_receivable_with_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
@@ -151,11 +152,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, True)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
|
||||
|
||||
row = report[1][0]
|
||||
row = report[1][-1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note,
|
||||
[
|
||||
@@ -168,6 +173,105 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_accounts_receivable_without_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, False)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
row = report[1]
|
||||
self.assertTrue(len(row) == 0)
|
||||
|
||||
def test_accounts_receivable_with_partial_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(qty=2)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment[i - 1],
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertFalse(cr_note.update_outstanding_for_self)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [
|
||||
[200, 100, 0, 80, 20, self.debit_to],
|
||||
[200, 40, 0, 0, 40, self.debit_to],
|
||||
]
|
||||
|
||||
for i in range(2):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note[i - 1],
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.party_account,
|
||||
],
|
||||
)
|
||||
|
||||
def test_cr_note_flag_to_update_self(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group By"),
|
||||
fieldtype: "Select",
|
||||
options: ["Asset Category", "Asset"],
|
||||
default: "Asset Category",
|
||||
},
|
||||
{
|
||||
fieldname: "asset_category",
|
||||
label: __("Asset Category"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category",
|
||||
depends_on: "eval: doc.group_by == 'Asset Category'",
|
||||
},
|
||||
{
|
||||
fieldname: "asset",
|
||||
label: __("Asset"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,21 +14,28 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
return get_group_by_asset_category_data(filters)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
return get_group_by_asset_data(filters)
|
||||
|
||||
|
||||
def get_group_by_asset_category_data(filters):
|
||||
data = []
|
||||
|
||||
asset_categories = get_asset_categories(filters)
|
||||
assets = get_assets(filters)
|
||||
asset_categories = get_asset_categories_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_category(filters)
|
||||
|
||||
for asset_category in asset_categories:
|
||||
row = frappe._dict()
|
||||
# row.asset_category = asset_category
|
||||
row.update(asset_category)
|
||||
|
||||
row.cost_as_on_to_date = (
|
||||
flt(row.cost_as_on_from_date)
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(
|
||||
@@ -38,17 +45,19 @@ def get_data(filters):
|
||||
if asset["asset_category"] == asset_category.get("asset_category", "")
|
||||
)
|
||||
)
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
@@ -57,52 +66,71 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_categories(filters):
|
||||
def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
condition += " and a.asset_category = %(asset_category)s"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT asset_category,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
group by asset_category
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
@@ -114,14 +142,17 @@ def get_asset_categories(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets(filters):
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
@@ -130,6 +161,11 @@ def get_assets(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -149,15 +185,22 @@ def get_assets(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -165,51 +208,272 @@ def get_assets(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and a.name = %(asset)s"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT a.name,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition = f" and a.name = '{filters.get('asset')}'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.name as name,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition}
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_as_on_from_date_credit,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.name) as results
|
||||
group by results.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
columns = []
|
||||
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset"),
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "cost_as_on_from_date",
|
||||
"label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "value_as_on_from_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Purchase"),
|
||||
"fieldname": "cost_of_new_purchase",
|
||||
"label": _("Value of New Purchase"),
|
||||
"fieldname": "value_of_new_purchase",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Sold Asset"),
|
||||
"fieldname": "cost_of_sold_asset",
|
||||
"label": _("Value of Sold Asset"),
|
||||
"fieldname": "value_of_sold_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Scrapped Asset"),
|
||||
"fieldname": "cost_of_scrapped_asset",
|
||||
"label": _("Value of Scrapped Asset"),
|
||||
"fieldname": "value_of_scrapped_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "cost_as_on_to_date",
|
||||
"label": _("Value of New Capitalized Asset"),
|
||||
"fieldname": "value_of_capitalized_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Value as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "value_as_on_to_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
@@ -237,6 +501,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation eliminated via reversal"),
|
||||
"fieldname": "depreciation_eliminated_via_reversal",
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "net_asset_value_as_on_from_date",
|
||||
@@ -250,3 +520,5 @@ def get_columns(filters):
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
||||
["Purchase Invoice", "docstatus", "=", 1],
|
||||
["Purchase Invoice", "per_received", "<", 100],
|
||||
["Purchase Invoice", "update_stock", "=", 0],
|
||||
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
|
||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
||||
and ba.account=gl.account
|
||||
and b.{budget_against} = gl.{budget_against}
|
||||
and gl.fiscal_year between %s and %s
|
||||
and gl.is_cancelled = 0
|
||||
and b.{budget_against} = %s
|
||||
and exists(
|
||||
select
|
||||
|
||||
@@ -61,7 +61,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldtype: "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.set_filter_value("party", []);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -468,9 +468,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
data[key][rev_dr_or_cr] = 0
|
||||
data[key][rev_dr_or_cr + "_in_account_currency"] = 0
|
||||
|
||||
if data[key].against_voucher and gle.against_voucher:
|
||||
data[key].against_voucher += ", " + gle.against_voucher
|
||||
|
||||
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
|
||||
show_opening_entries = filters.get("show_opening_entries")
|
||||
|
||||
@@ -506,6 +503,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
keylist.append(gle.get("project"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
@@ -617,10 +615,11 @@ def get_columns(filters):
|
||||
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
||||
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
||||
{"label": _("Party"), "fieldname": "party", "width": 100},
|
||||
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
|
||||
]
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
|
||||
|
||||
for dim in get_accounting_dimensions(as_list=False):
|
||||
columns.append(
|
||||
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
||||
@@ -631,14 +630,6 @@ def get_columns(filters):
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100},
|
||||
{
|
||||
"label": _("Against Voucher"),
|
||||
"fieldname": "against_voucher",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "against_voucher_type",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
|
||||
]
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
add_sub_total_row,
|
||||
add_total_row,
|
||||
apply_group_by_conditions,
|
||||
apply_order_by_conditions,
|
||||
get_grand_total,
|
||||
get_group_by_and_display_fields,
|
||||
get_tax_accounts,
|
||||
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
query = apply_conditions(query, pi, pii, filters)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
|
||||
@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||
)
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(si.posting_date, order=Order.desc)
|
||||
query = query.orderby(sii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, si, sii, filters)
|
||||
|
||||
for key, value in (additional_conditions or {}).items():
|
||||
query = query.where(si[key] == value)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def apply_group_by_conditions(query, si, ii, filters):
|
||||
if filters.get("group_by") == "Invoice":
|
||||
query = query.orderby(ii.parent, order=Order.desc)
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query = query.orderby(ii.item_code)
|
||||
query += f" order by {ii.item_code}"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query = query.orderby(ii.item_group)
|
||||
query += f" order by {ii.item_group}"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
|
||||
return query
|
||||
|
||||
@@ -479,7 +476,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.orderby(pi.posting_date, pi.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -413,8 +412,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -438,7 +438,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
si.company,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.orderby(si.posting_date, si.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -453,8 +452,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -1788,14 +1788,17 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
):
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
outstanding_amount = flt(
|
||||
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
||||
)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
|
||||
ref_doc.outstanding_amount = outstanding_amount
|
||||
frappe.db.set_value(
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
"outstanding_amount",
|
||||
outstanding["outstanding_in_account_currency"] or 0.0,
|
||||
outstanding_amount,
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
@@ -69,6 +69,7 @@ class AssetCapitalization(StockController):
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -82,6 +83,7 @@ class AssetCapitalization(StockController):
|
||||
self.cancel_target_asset()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def on_trash(self):
|
||||
|
||||
@@ -153,6 +153,48 @@ class AccountsController(TransactionBase):
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
def validate_against_voucher_outstanding(self):
|
||||
from frappe.model.meta import get_meta
|
||||
|
||||
if not get_meta(self.doctype).has_field("outstanding_amount"):
|
||||
return
|
||||
|
||||
if self.get("is_return") and self.return_against and not self.get("is_pos"):
|
||||
against_voucher_outstanding = frappe.get_value(
|
||||
self.doctype, self.return_against, "outstanding_amount"
|
||||
)
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
|
||||
msg = ""
|
||||
if self.get("update_outstanding_for_self"):
|
||||
msg = (
|
||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
|
||||
"uncheck '{2}' checkbox. <br><br>Or"
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold(_("Update Outstanding for Self")),
|
||||
)
|
||||
|
||||
elif not self.update_outstanding_for_self and (
|
||||
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
|
||||
):
|
||||
self.update_outstanding_for_self = 1
|
||||
msg = (
|
||||
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
|
||||
).format(
|
||||
against_voucher_outstanding,
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
flt(abs(self.outstanding_amount)),
|
||||
)
|
||||
|
||||
if msg:
|
||||
msg += " you can use {} tool to reconcile against {} later.".format(
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
)
|
||||
frappe.msgprint(_(msg))
|
||||
|
||||
def validate(self):
|
||||
if not self.get("is_return") and not self.get("is_debit_note"):
|
||||
self.validate_qty_is_not_zero()
|
||||
@@ -178,6 +220,7 @@ class AccountsController(TransactionBase):
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
self.init_internal_values()
|
||||
self.validate_against_voucher_outstanding()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
self.calculate_taxes_and_totals()
|
||||
@@ -209,20 +252,6 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
||||
if self.get("update_outstanding_for_self"):
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold("Update Outstanding for Self"),
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
)
|
||||
)
|
||||
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
self.set_advances()
|
||||
@@ -2100,7 +2129,9 @@ class AccountsController(TransactionBase):
|
||||
and automatically_fetch_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
if self.get("payment_terms_template"):
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
elif self.get("payment_terms_template"):
|
||||
@@ -2141,7 +2172,9 @@ class AccountsController(TransactionBase):
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
else:
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
)
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
|
||||
def get_order_details(self):
|
||||
@@ -2179,7 +2212,9 @@ class AccountsController(TransactionBase):
|
||||
def linked_order_has_payment_schedule(self, po_or_so):
|
||||
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
||||
|
||||
def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype):
|
||||
def fetch_payment_terms_from_order(
|
||||
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
):
|
||||
"""
|
||||
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
|
||||
"""
|
||||
@@ -2195,12 +2230,25 @@ class AccountsController(TransactionBase):
|
||||
"invoice_portion": schedule.invoice_portion,
|
||||
"mode_of_payment": schedule.mode_of_payment,
|
||||
"description": schedule.description,
|
||||
"payment_amount": schedule.payment_amount,
|
||||
"base_payment_amount": schedule.base_payment_amount,
|
||||
"outstanding": schedule.outstanding,
|
||||
"paid_amount": schedule.paid_amount,
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
)
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
||||
else:
|
||||
payment_schedule["base_payment_amount"] = flt(
|
||||
schedule.base_payment_amount * self.get("conversion_rate"),
|
||||
schedule.precision("base_payment_amount"),
|
||||
)
|
||||
|
||||
if schedule.discount_type == "Percentage":
|
||||
payment_schedule["discount_type"] = schedule.discount_type
|
||||
payment_schedule["discount"] = schedule.discount
|
||||
@@ -2459,12 +2507,17 @@ class AccountsController(TransactionBase):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
if not self.conversion_rate:
|
||||
throw(_("Conversion rate cannot be 0"))
|
||||
|
||||
if self.currency == default_currency and flt(self.conversion_rate) != 1.00:
|
||||
throw(_("Conversion rate must be 1.00 if document currency is same as company currency"))
|
||||
|
||||
if self.currency != default_currency and flt(self.conversion_rate) == 1.00:
|
||||
frappe.msgprint(
|
||||
_("Conversion rate is 1.00, but document currency is different from company currency")
|
||||
)
|
||||
|
||||
def check_finance_books(self, item, asset):
|
||||
if (
|
||||
|
||||
@@ -801,7 +801,7 @@ class StockController(AccountsController):
|
||||
child_tab.item_code,
|
||||
child_tab.qty,
|
||||
)
|
||||
.where(parent_tab.docstatus < 2)
|
||||
.where(parent_tab.docstatus == 1)
|
||||
)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
|
||||
@@ -373,9 +373,7 @@ class calculate_taxes_and_totals:
|
||||
self._calculate()
|
||||
|
||||
def calculate_taxes(self):
|
||||
rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment")
|
||||
if not rounding_adjustment_computed:
|
||||
self.doc.rounding_adjustment = 0
|
||||
self.grand_total_diff = 0
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
actual_tax_dict = dict(
|
||||
@@ -440,9 +438,8 @@ class calculate_taxes_and_totals:
|
||||
and self.discount_amount_applied
|
||||
and self.doc.discount_amount
|
||||
and self.doc.apply_discount_on == "Grand Total"
|
||||
and not rounding_adjustment_computed
|
||||
):
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.grand_total_diff = flt(
|
||||
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"),
|
||||
)
|
||||
@@ -528,11 +525,11 @@ class calculate_taxes_and_totals:
|
||||
return self.adjust_grand_total_for_inclusive_tax()
|
||||
|
||||
def adjust_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
# if any inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
non_inclusive_tax_amount = sum(
|
||||
flt(d.tax_amount_after_discount_amount)
|
||||
self.get_tax_amount_if_for_valuation_or_deduction(d.tax_amount_after_discount_amount, d)
|
||||
for d in self.doc.get("taxes")
|
||||
if not d.included_in_print_rate
|
||||
)
|
||||
@@ -549,27 +546,23 @@ class calculate_taxes_and_totals:
|
||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||
|
||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||
self.doc.grand_total_diff = diff
|
||||
else:
|
||||
self.doc.grand_total_diff = 0
|
||||
self.grand_total_diff = diff
|
||||
|
||||
def calculate_totals(self):
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
|
||||
self.doc.get("grand_total_diff")
|
||||
)
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
|
||||
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
|
||||
self.doc.precision("total_taxes_and_charges"),
|
||||
)
|
||||
else:
|
||||
self.doc.total_taxes_and_charges = 0.0
|
||||
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges"])
|
||||
|
||||
if self.doc.doctype in [
|
||||
"Quotation",
|
||||
@@ -619,7 +612,9 @@ class calculate_taxes_and_totals:
|
||||
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
self.doc.rounded_total = 0
|
||||
self.doc.base_rounded_total = 0
|
||||
self.doc.rounding_adjustment = 0
|
||||
return
|
||||
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||
@@ -663,33 +658,29 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
total_for_discount_amount = self.get_total_for_discount_amount()
|
||||
taxes = self.doc.get("taxes")
|
||||
net_total = 0
|
||||
expected_net_total = 0
|
||||
|
||||
if total_for_discount_amount:
|
||||
# calculate item amount after Discount Amount
|
||||
for i, item in enumerate(self._items):
|
||||
for item in self._items:
|
||||
distributed_amount = (
|
||||
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
|
||||
)
|
||||
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount"))
|
||||
adjusted_net_amount = item.net_amount - distributed_amount
|
||||
expected_net_total += adjusted_net_amount
|
||||
item.net_amount = flt(adjusted_net_amount, item.precision("net_amount"))
|
||||
net_total += item.net_amount
|
||||
|
||||
# discount amount rounding loss adjustment if no taxes
|
||||
if (
|
||||
self.doc.apply_discount_on == "Net Total"
|
||||
or not taxes
|
||||
or total_for_discount_amount == self.doc.net_total
|
||||
) and i == len(self._items) - 1:
|
||||
discount_amount_loss = flt(
|
||||
self.doc.net_total - net_total - self.doc.discount_amount,
|
||||
self.doc.precision("net_total"),
|
||||
)
|
||||
|
||||
# discount amount rounding adjustment
|
||||
if rounding_difference := flt(
|
||||
expected_net_total - net_total, self.doc.precision("net_total")
|
||||
):
|
||||
item.net_amount = flt(
|
||||
item.net_amount + discount_amount_loss, item.precision("net_amount")
|
||||
item.net_amount + rounding_difference, item.precision("net_amount")
|
||||
)
|
||||
net_total += rounding_difference
|
||||
|
||||
item.net_rate = (
|
||||
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0
|
||||
@@ -705,20 +696,44 @@ class calculate_taxes_and_totals:
|
||||
def get_total_for_discount_amount(self):
|
||||
if self.doc.apply_discount_on == "Net Total":
|
||||
return self.doc.net_total
|
||||
else:
|
||||
actual_taxes_dict = {}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax)
|
||||
actual_taxes_dict.setdefault(tax.idx, tax_amount)
|
||||
elif tax.row_id in actual_taxes_dict:
|
||||
actual_tax_amount = flt(actual_taxes_dict.get(tax.row_id, 0)) * flt(tax.rate) / 100
|
||||
actual_taxes_dict.setdefault(tax.idx, actual_tax_amount)
|
||||
total_actual_tax = 0
|
||||
actual_taxes_dict = {}
|
||||
|
||||
return flt(
|
||||
self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total")
|
||||
def update_actual_tax_dict(tax, tax_amount):
|
||||
nonlocal total_actual_tax
|
||||
|
||||
if tax.get("add_deduct_tax") == "Deduct":
|
||||
tax_amount *= -1
|
||||
|
||||
if tax.get("category") != "Valuation":
|
||||
total_actual_tax += tax_amount
|
||||
|
||||
actual_taxes_dict[int(tax.idx)] = {
|
||||
"tax_amount": tax_amount,
|
||||
"cumulative_tax_amount": total_actual_tax,
|
||||
}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
update_actual_tax_dict(tax, tax.tax_amount)
|
||||
continue
|
||||
|
||||
if not tax.row_id:
|
||||
continue
|
||||
|
||||
base_row = actual_taxes_dict.get(int(tax.row_id))
|
||||
if not base_row:
|
||||
continue
|
||||
|
||||
base_tax_amount = (
|
||||
base_row["tax_amount"]
|
||||
if tax.charge_type == "On Previous Row Amount"
|
||||
else base_row["cumulative_tax_amount"]
|
||||
)
|
||||
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
|
||||
|
||||
return self.doc.grand_total - total_actual_tax
|
||||
|
||||
def calculate_total_advance(self):
|
||||
if not self.doc.docstatus.is_cancelled():
|
||||
@@ -780,9 +795,12 @@ class calculate_taxes_and_totals:
|
||||
if (
|
||||
self.doc.is_return
|
||||
and self.doc.return_against
|
||||
and not self.doc.update_outstanding_for_self
|
||||
and not self.doc.get("is_pos")
|
||||
or self.is_internal_invoice()
|
||||
):
|
||||
# Do not calculate the outstanding amount for a return invoice if 'update_outstanding_for_self' is not enabled.
|
||||
self.doc.outstanding_amount = 0
|
||||
return
|
||||
|
||||
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
|
||||
|
||||
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
|
||||
app_description = """ERP made simple"""
|
||||
app_icon = "fa fa-th"
|
||||
app_color = "#e74c3c"
|
||||
app_email = "info@erpnext.com"
|
||||
app_email = "hello@frappe.io"
|
||||
app_license = "GNU General Public License (v3)"
|
||||
source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
@@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
|
||||
default_mail_footer = """
|
||||
<span>
|
||||
Sent via
|
||||
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank">
|
||||
<a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
|
||||
ERPNext
|
||||
</a>
|
||||
</span>
|
||||
|
||||
@@ -92,7 +92,7 @@ class WorkOrder(Document):
|
||||
if self.source_warehouse:
|
||||
self.set_warehouses()
|
||||
|
||||
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
||||
validate_uom_is_integer(self, "stock_uom", ["required_qty"])
|
||||
|
||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||
|
||||
|
||||
@@ -371,4 +371,4 @@ erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
|
||||
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
|
||||
@@ -174,7 +174,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
}
|
||||
|
||||
qty(doc, cdt, cdn) {
|
||||
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) {
|
||||
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
|
||||
this.calculate_received_qty(doc, cdt, cdn)
|
||||
}
|
||||
super.qty(doc, cdt, cdn);
|
||||
|
||||
@@ -336,7 +336,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
calculate_taxes() {
|
||||
var me = this;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
this.grand_total_diff = 0;
|
||||
var actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
@@ -407,7 +407,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
// adjust Discount Amount loss in last tax iteration
|
||||
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
|
||||
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
|
||||
me.frm.doc.rounding_adjustment = flt(me.frm.doc.grand_total -
|
||||
me.grand_total_diff = flt(me.frm.doc.grand_total -
|
||||
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
|
||||
}
|
||||
}
|
||||
@@ -519,7 +519,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
adjust_grand_total_for_inclusive_tax() {
|
||||
var me = this;
|
||||
|
||||
// if fully inclusive taxes and diff
|
||||
// if any inclusive taxes and diff
|
||||
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
||||
var any_inclusive_tax = false;
|
||||
$.each(this.frm.doc.taxes || [], function(i, d) {
|
||||
@@ -530,7 +530,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
|
||||
function(d) {
|
||||
if(!d.included_in_print_rate) {
|
||||
return flt(d.tax_amount_after_discount_amount);
|
||||
let tax_amount = d.category === "Valuation" ? 0 : d.tax_amount_after_discount_amount;
|
||||
if (d.add_deduct_tax === "Deduct") tax_amount *= -1;
|
||||
return tax_amount;
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -544,9 +546,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
diff = flt(diff, precision("rounding_adjustment"));
|
||||
|
||||
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
||||
me.frm.doc.grand_total_diff = diff;
|
||||
} else {
|
||||
me.frm.doc.grand_total_diff = 0;
|
||||
me.grand_total_diff = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,7 +557,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var me = this;
|
||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||
this.frm.doc.grand_total = flt(tax_count
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
@@ -589,9 +589,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
||||
- flt(this.frm.doc.rounding_adjustment), precision("total_taxes_and_charges"));
|
||||
- this.grand_total_diff, precision("total_taxes_and_charges"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges", "rounding_adjustment"]);
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
|
||||
|
||||
// Round grand total as per precision
|
||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
|
||||
@@ -611,6 +611,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (cint(disable_rounded_total)) {
|
||||
this.frm.doc.rounded_total = 0;
|
||||
this.frm.doc.base_rounded_total = 0;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -679,22 +680,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
return;
|
||||
}
|
||||
|
||||
var total_for_discount_amount = this.get_total_for_discount_amount();
|
||||
var net_total = 0;
|
||||
const total_for_discount_amount = this.get_total_for_discount_amount();
|
||||
let net_total = 0;
|
||||
let expected_net_total = 0;
|
||||
|
||||
// calculate item amount after Discount Amount
|
||||
if (total_for_discount_amount) {
|
||||
$.each(this.frm._items || [], function(i, item) {
|
||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
|
||||
|
||||
const adjusted_net_amount = item.net_amount - distributed_amount;
|
||||
expected_net_total += adjusted_net_amount
|
||||
item.net_amount = flt(adjusted_net_amount, precision("net_amount", item));
|
||||
net_total += item.net_amount;
|
||||
|
||||
// discount amount rounding loss adjustment if no taxes
|
||||
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
||||
&& i == (me.frm._items || []).length - 1) {
|
||||
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
||||
- me.frm.doc.discount_amount, precision("net_total"));
|
||||
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
||||
precision("net_amount", item));
|
||||
// discount amount rounding adjustment
|
||||
// assignment to rounding_difference is intentional
|
||||
const rounding_difference = flt(expected_net_total - net_total, precision("net_total"));
|
||||
if (rounding_difference) {
|
||||
item.net_amount = flt(item.net_amount + rounding_difference, precision("net_amount", item));
|
||||
net_total += rounding_difference;
|
||||
}
|
||||
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
||||
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
|
||||
@@ -707,29 +712,38 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
get_total_for_discount_amount() {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total") {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total")
|
||||
return this.frm.doc.net_total;
|
||||
} else {
|
||||
var total_actual_tax = 0.0;
|
||||
var actual_taxes_dict = {};
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
|
||||
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
actual_taxes_dict[tax.idx] = tax_amount;
|
||||
} else if (actual_taxes_dict[tax.row_id] !== null) {
|
||||
var actual_tax_amount = flt(actual_taxes_dict[tax.row_id]) * flt(tax.rate) / 100;
|
||||
actual_taxes_dict[tax.idx] = actual_tax_amount;
|
||||
}
|
||||
});
|
||||
let total_actual_tax = 0.0;
|
||||
let actual_taxes_dict = {};
|
||||
|
||||
$.each(actual_taxes_dict, function(key, value) {
|
||||
if (value) total_actual_tax += value;
|
||||
});
|
||||
function update_actual_taxes_dict(tax, tax_amount) {
|
||||
if (tax.add_deduct_tax == "Deduct") tax_amount *= -1;
|
||||
if (tax.category != "Valuation") total_actual_tax += tax_amount;
|
||||
|
||||
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
|
||||
actual_taxes_dict[tax.idx] = {
|
||||
tax_amount: tax_amount,
|
||||
cumulative_total: total_actual_tax
|
||||
};
|
||||
}
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
update_actual_taxes_dict(tax, tax.tax_amount);
|
||||
return;
|
||||
}
|
||||
|
||||
const base_row = actual_taxes_dict[tax.row_id];
|
||||
if (!base_row) return;
|
||||
|
||||
// if charge type is 'On Previous Row Amount', calculate tax on previous row amount
|
||||
// else (On Previous Row Total) calculate tax on cumulative total
|
||||
const base_tax_amount = tax.charge_type == "On Previous Row Amount" ? base_row["tax_amount"]: base_row["cumulative_total"];
|
||||
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
|
||||
});
|
||||
|
||||
return this.frm.doc.grand_total - total_actual_tax;
|
||||
}
|
||||
|
||||
calculate_total_advance(update_paid_amount) {
|
||||
|
||||
@@ -297,7 +297,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
docstatus: ["<", 2],
|
||||
inspection_type: inspection_type,
|
||||
reference_name: doc.name,
|
||||
item_code: d.item_code
|
||||
|
||||
@@ -750,7 +750,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
});
|
||||
}
|
||||
|
||||
new frappe.ui.Dialog({
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Items"),
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
@@ -787,7 +787,9 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
refresh_field("items");
|
||||
},
|
||||
primary_action_label: __("Update"),
|
||||
}).show();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
};
|
||||
|
||||
erpnext.utils.map_current_doc = function (opts) {
|
||||
|
||||
@@ -121,10 +121,10 @@ erpnext.accounts.unreconcile_payment = {
|
||||
};
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "UnReconcile Allocations",
|
||||
title: __("UnReconcile Allocations"),
|
||||
fields: unreconcile_dialog_fields,
|
||||
size: "large",
|
||||
primary_action_label: "UnReconcile",
|
||||
primary_action_label: __("UnReconcile"),
|
||||
primary_action(values) {
|
||||
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
||||
if (selected_allocations.length > 0) {
|
||||
@@ -138,7 +138,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
);
|
||||
d.hide();
|
||||
} else {
|
||||
frappe.msgprint("No Selection");
|
||||
frappe.msgprint(__("No Selection"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
|
||||
{% if country != "United States" %}{{ country }}{% endif -%}
|
||||
|
||||
@@ -18,7 +18,6 @@ frappe.ui.form.on("Customer", {
|
||||
|
||||
frm.add_fetch("lead_name", "company_name", "customer_name");
|
||||
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate");
|
||||
frm.set_query("customer_group", { is_group: 0 });
|
||||
frm.set_query("default_price_list", { selling: 1 });
|
||||
frm.set_query("account", "accounts", function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
||||
@@ -275,12 +275,12 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_all(
|
||||
invoices_by_customer = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||
fields=fields,
|
||||
)
|
||||
invoices_by_name = frappe.db.get_all(
|
||||
invoices_by_name = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
||||
fields=fields,
|
||||
@@ -288,7 +288,7 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
|
||||
invoice_list = invoices_by_customer + invoices_by_name
|
||||
elif status:
|
||||
invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields)
|
||||
invoice_list = frappe.db.get_list("POS Invoice", filters={"status": status}, fields=fields)
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
@@ -190,35 +190,42 @@ class TransactionDeletionRecord(Document):
|
||||
"""Delete addresses to which leads are linked"""
|
||||
self.validate_doc_status()
|
||||
if not self.delete_leads_and_addresses:
|
||||
leads = frappe.get_all("Lead", filters={"company": self.company})
|
||||
leads = ["'%s'" % row.get("name") for row in leads]
|
||||
leads = frappe.db.get_all("Lead", filters={"company": self.company}, pluck="name")
|
||||
addresses = []
|
||||
if leads:
|
||||
addresses = frappe.db.sql_list(
|
||||
"""select parent from `tabDynamic Link` where link_name
|
||||
in ({leads})""".format(leads=",".join(leads))
|
||||
addresses = frappe.db.get_all(
|
||||
"Dynamic Link", filters={"link_name": ("in", leads)}, pluck="parent"
|
||||
)
|
||||
|
||||
if addresses:
|
||||
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tabAddress` where name in ({addresses}) and
|
||||
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
|
||||
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
|
||||
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))
|
||||
)
|
||||
address = qb.DocType("Address")
|
||||
dl1 = qb.DocType("Dynamic Link")
|
||||
dl2 = qb.DocType("Dynamic Link")
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tabDynamic Link` where link_doctype='Lead'
|
||||
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))
|
||||
)
|
||||
qb.from_(address).delete().where(
|
||||
(address.name.isin(addresses))
|
||||
& (
|
||||
address.name.notin(
|
||||
qb.from_(dl1)
|
||||
.join(dl2)
|
||||
.on((dl1.parent == dl2.parent) & (dl1.link_doctype != dl2.link_doctype))
|
||||
.select(dl1.parent)
|
||||
.distinct()
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
dynamic_link = qb.DocType("Dynamic Link")
|
||||
qb.from_(dynamic_link).delete().where(
|
||||
(dynamic_link.link_doctype == "Lead")
|
||||
& (dynamic_link.parenttype == "Address")
|
||||
& (dynamic_link.link_name.isin(leads))
|
||||
).run()
|
||||
|
||||
customer = qb.DocType("Customer")
|
||||
qb.update(customer).set(customer.lead_name, None).where(customer.lead_name.isin(leads)).run()
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
|
||||
leads=",".join(leads)
|
||||
)
|
||||
)
|
||||
self.db_set("delete_leads_and_addresses", 1)
|
||||
self.enqueue_task(task="Reset Company Values")
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
|
||||
from .default_success_action import get_default_success_action
|
||||
|
||||
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
|
||||
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
|
||||
<a style="color: #888" href="http://frappe.io/erpnext">ERPNext</a></div>"""
|
||||
|
||||
|
||||
def after_install():
|
||||
|
||||
@@ -196,8 +196,17 @@ class InventoryDimension(Document):
|
||||
custom_fields["Stock Ledger Entry"] = dimension_field
|
||||
|
||||
filter_custom_fields = {}
|
||||
|
||||
ignore_doctypes = [
|
||||
"Pick List Item",
|
||||
"Maintenance Visit Purpose",
|
||||
]
|
||||
|
||||
if custom_fields:
|
||||
for doctype, fields in custom_fields.items():
|
||||
if doctype in ignore_doctypes:
|
||||
continue
|
||||
|
||||
if isinstance(fields, dict):
|
||||
fields = [fields]
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Pick List", {
|
||||
after_save(frm) {
|
||||
setTimeout(() => {
|
||||
// Added to fix the issue of locations table not getting updated after save
|
||||
frm.reload_doc();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
setup: (frm) => {
|
||||
frm.set_indicator_formatter("item_code", function (doc) {
|
||||
return doc.stock_qty === 0 ? "red" : "green";
|
||||
|
||||
@@ -25,10 +25,38 @@ from erpnext.stock.get_item_details import get_conversion_factor
|
||||
|
||||
class PickList(Document):
|
||||
def validate(self):
|
||||
self.validate_expired_batches()
|
||||
self.validate_for_qty()
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
|
||||
def validate_expired_batches(self):
|
||||
batches = []
|
||||
for row in self.get("locations"):
|
||||
if row.get("batch_no") and row.get("picked_qty"):
|
||||
batches.append(row.batch_no)
|
||||
|
||||
if batches:
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
query = (
|
||||
frappe.qb.from_(batch)
|
||||
.select(batch.name)
|
||||
.where(
|
||||
(batch.name.isin(batches))
|
||||
& (batch.expiry_date <= frappe.utils.nowdate())
|
||||
& (batch.expiry_date.isnotnull())
|
||||
)
|
||||
)
|
||||
|
||||
expired_batches = query.run(as_dict=True)
|
||||
if expired_batches:
|
||||
msg = "<ul>" + "".join(f"<li>{batch.name}</li>" for batch in expired_batches) + "</ul>"
|
||||
|
||||
frappe.throw(
|
||||
_("The following batches are expired, please restock them: <br> {0}").format(msg),
|
||||
title=_("Expired Batches"),
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
if not self.pick_manually:
|
||||
@@ -271,6 +299,7 @@ class PickList(Document):
|
||||
self.remove(row)
|
||||
|
||||
updated_locations = frappe._dict()
|
||||
len_idx = len(self.get("locations")) or 0
|
||||
for item_doc in items:
|
||||
item_code = item_doc.item_code
|
||||
|
||||
@@ -313,6 +342,8 @@ class PickList(Document):
|
||||
if location.picked_qty > location.stock_qty:
|
||||
location.picked_qty = location.stock_qty
|
||||
|
||||
len_idx += 1
|
||||
location.idx = len_idx
|
||||
self.append("locations", location)
|
||||
|
||||
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
|
||||
@@ -321,6 +352,9 @@ class PickList(Document):
|
||||
for location in locations_replica:
|
||||
location.stock_qty = 0
|
||||
location.picked_qty = 0
|
||||
|
||||
len_idx += 1
|
||||
location.idx = len_idx
|
||||
self.append("locations", location)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
@@ -430,9 +464,11 @@ class PickList(Document):
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
|
||||
"picked_qty"
|
||||
),
|
||||
Sum(
|
||||
Case()
|
||||
.when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
|
||||
.else_(pi_item.stock_qty)
|
||||
).as_("picked_qty"),
|
||||
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
|
||||
)
|
||||
.where(
|
||||
@@ -465,8 +501,32 @@ class PickList(Document):
|
||||
else:
|
||||
picked_items[item_data.item_code][key] = data
|
||||
|
||||
self.update_picked_item_from_current_pick_list(picked_items)
|
||||
|
||||
return picked_items
|
||||
|
||||
def update_picked_item_from_current_pick_list(self, picked_items):
|
||||
for row in self.get("locations"):
|
||||
if flt(row.picked_qty) > 0:
|
||||
key = (row.warehouse, row.batch_no) if row.batch_no else row.warehouse
|
||||
serial_no = [x for x in row.serial_no.split("\n") if x] if row.serial_no else None
|
||||
if row.item_code not in picked_items:
|
||||
picked_items[row.item_code] = {}
|
||||
|
||||
if key not in picked_items[row.item_code]:
|
||||
picked_items[row.item_code][key] = frappe._dict(
|
||||
{
|
||||
"picked_qty": 0,
|
||||
"serial_no": [],
|
||||
"batch_no": row.batch_no or "",
|
||||
"warehouse": row.warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
picked_items[row.item_code][key]["picked_qty"] += flt(row.stock_qty) or flt(row.picked_qty)
|
||||
if serial_no:
|
||||
picked_items[row.item_code][key]["serial_no"].extend(serial_no)
|
||||
|
||||
def _get_product_bundles(self) -> dict[str, str]:
|
||||
# Dict[so_item_row: item_code]
|
||||
product_bundles = {}
|
||||
|
||||
@@ -865,15 +865,19 @@ def get_billed_amount_against_po(po_items):
|
||||
if not po_items:
|
||||
return {}
|
||||
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(purchase_invoice_item)
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice_item.parent == purchase_invoice.name)
|
||||
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
|
||||
.where(
|
||||
(purchase_invoice_item.po_detail.isin(po_items))
|
||||
& (purchase_invoice_item.docstatus == 1)
|
||||
& (purchase_invoice.docstatus == 1)
|
||||
& (purchase_invoice_item.pr_detail.isnull())
|
||||
& (purchase_invoice.update_stock == 0)
|
||||
)
|
||||
.groupby(purchase_invoice_item.po_detail)
|
||||
).run(as_dict=1)
|
||||
|
||||
@@ -2731,6 +2731,36 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
self.assertEqual(return_pr.per_billed, 100)
|
||||
self.assertEqual(return_pr.status, "Completed")
|
||||
|
||||
def test_pr_status_based_on_invoices_with_update_stock(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as _make_purchase_invoice,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_receipt as _make_purchase_receipt,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
|
||||
item_code = "Test Item for PR Status Based on Invoices"
|
||||
create_item(item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=10)
|
||||
pi = _make_purchase_invoice(po.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].qty = 5
|
||||
pi.submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.per_billed, 50)
|
||||
|
||||
pr = _make_purchase_receipt(po.name)
|
||||
self.assertEqual(pr.items[0].qty, 5)
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
self.assertEqual(pr.status, "To Bill")
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -49,6 +49,9 @@ class QualityInspection(Document):
|
||||
child = self.append("readings", {})
|
||||
child.update(d)
|
||||
child.status = "Accepted"
|
||||
child.parameter_group = frappe.get_value(
|
||||
"Quality Inspection Parameter", d.specification, "parameter_group"
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_quality_inspection_template(self):
|
||||
|
||||
@@ -209,7 +209,7 @@ class SerialNo(StockController):
|
||||
OR serial_no like %s
|
||||
)
|
||||
ORDER BY
|
||||
posting_date desc, posting_time desc, creation desc""",
|
||||
posting_datetime desc, creation desc""",
|
||||
(
|
||||
self.item_code,
|
||||
self.company,
|
||||
@@ -279,7 +279,14 @@ def validate_serial_no(sle, item_det):
|
||||
_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)
|
||||
)
|
||||
|
||||
if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)):
|
||||
if (
|
||||
(
|
||||
(sle.voucher_type == "Stock Reconciliation" and sle.actual_qty > 0)
|
||||
or sle.voucher_type != "Stock Reconciliation"
|
||||
)
|
||||
and len(serial_nos)
|
||||
and len(serial_nos) != abs(cint(sle.actual_qty))
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
|
||||
abs(sle.actual_qty), sle.item_code, len(serial_nos)
|
||||
|
||||
@@ -1314,17 +1314,38 @@ class StockEntry(StockController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(self, args=None, for_update=False):
|
||||
item = frappe.db.sql(
|
||||
"""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
|
||||
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
|
||||
id.expense_account, id.buying_cost_center
|
||||
from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s
|
||||
where i.name=%s
|
||||
and i.disabled=0
|
||||
and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""",
|
||||
(self.company, args.get("item_code"), nowdate()),
|
||||
as_dict=1,
|
||||
item = frappe.qb.DocType("Item")
|
||||
item_default = frappe.qb.DocType("Item Default")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(item)
|
||||
.left_join(item_default)
|
||||
.on((item.name == item_default.parent) & (item_default.company == self.company))
|
||||
.select(
|
||||
item.name,
|
||||
item.stock_uom,
|
||||
item.description,
|
||||
item.image,
|
||||
item.item_name,
|
||||
item.item_group,
|
||||
item.has_batch_no,
|
||||
item.sample_quantity,
|
||||
item.has_serial_no,
|
||||
item.allow_alternative_item,
|
||||
item_default.expense_account,
|
||||
item_default.buying_cost_center,
|
||||
)
|
||||
.where(
|
||||
(item.name == args.get("item_code"))
|
||||
& (item.disabled == 0)
|
||||
& (
|
||||
(item.end_of_life.isnull())
|
||||
| (item.end_of_life < "1900-01-01")
|
||||
| (item.end_of_life > nowdate())
|
||||
)
|
||||
)
|
||||
)
|
||||
item = query.run(as_dict=True)
|
||||
|
||||
if not item:
|
||||
frappe.throw(
|
||||
@@ -1369,6 +1390,11 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Material Issue":
|
||||
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
ret["expense_account"] = frappe.get_cached_value(
|
||||
"Company", self.company, "stock_adjustment_account"
|
||||
)
|
||||
|
||||
for company_field, field in {
|
||||
"stock_adjustment_account": "expense_account",
|
||||
"cost_center": "cost_center",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
|
||||
on_doctype_update as create_sle_indexes,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
"""Ensure SLE Indexes"""
|
||||
|
||||
create_sle_indexes()
|
||||
@@ -81,5 +81,11 @@ frappe.query_reports["Stock Ageing"] = {
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_closing_balance",
|
||||
label: __("Ignore Closing Balance"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ from operator import itemgetter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, date_diff, flt, get_datetime
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import add_days, cint, date_diff, flt, get_date_str, get_datetime, getdate
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -49,7 +50,13 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
|
||||
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
||||
range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
|
||||
|
||||
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
|
||||
row = [
|
||||
details.name or details.item_code,
|
||||
details.item_name,
|
||||
details.description,
|
||||
details.item_group,
|
||||
details.brand,
|
||||
]
|
||||
|
||||
if filters.get("show_warehouse_wise_stock"):
|
||||
row.append(details.warehouse)
|
||||
@@ -217,6 +224,67 @@ class FIFOSlots:
|
||||
self.filters = filters
|
||||
self.sle = sle
|
||||
|
||||
def get_closing_balance(self):
|
||||
if self.filters.get("ignore_closing_balance"):
|
||||
return []
|
||||
|
||||
if (
|
||||
self.filters.get("item_code")
|
||||
or self.filters.get("warehouse")
|
||||
or self.filters.get("warehouse_type")
|
||||
):
|
||||
return
|
||||
|
||||
if self.sle:
|
||||
return
|
||||
|
||||
table = frappe.qb.DocType("Closing Stock Balance")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name, table.to_date)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.company == self.filters.company)
|
||||
& (table.to_date < self.filters.get("to_date"))
|
||||
& (table.status == "Completed")
|
||||
)
|
||||
.orderby(table.to_date, order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
|
||||
if self.filters.get(fieldname):
|
||||
query = query.where(table[fieldname] == self.filters.get(fieldname))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def prepare_stock_ageing_from_stock_closing_balance(self):
|
||||
closing_balance = self.get_closing_balance()
|
||||
if not closing_balance:
|
||||
return
|
||||
|
||||
self.start_from = add_days(closing_balance[0].to_date, 1)
|
||||
closing_data = frappe.get_doc("Closing Stock Balance", closing_balance[0].name).get_prepared_data()
|
||||
stock_ledger_entries = closing_data.get("data")
|
||||
|
||||
for d in stock_ledger_entries:
|
||||
if isinstance(d, dict):
|
||||
d = frappe._dict(d)
|
||||
|
||||
d.actual_qty = d.bal_qty
|
||||
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
|
||||
serial_nos = d.serial_no if d.serial_no else []
|
||||
if fifo_queue and isinstance(fifo_queue[0][0], str):
|
||||
d.has_serial_no = 1
|
||||
|
||||
if d.actual_qty > 0:
|
||||
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
|
||||
else:
|
||||
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
|
||||
|
||||
self.__update_balances(d, key)
|
||||
|
||||
def generate(self) -> dict:
|
||||
"""
|
||||
Returns dict of the foll.g structure:
|
||||
@@ -227,6 +295,9 @@ class FIFOSlots:
|
||||
consumed/updated and maintained via FIFO. **
|
||||
}
|
||||
"""
|
||||
self.start_from = None
|
||||
self.prepare_stock_ageing_from_stock_closing_balance()
|
||||
|
||||
stock_ledger_entries = self.sle
|
||||
|
||||
_system_settings = frappe.get_cached_doc("System Settings")
|
||||
@@ -259,15 +330,32 @@ class FIFOSlots:
|
||||
|
||||
return self.item_details
|
||||
|
||||
def format_fifo_queue(self, fifo_queue: list) -> list:
|
||||
if not fifo_queue:
|
||||
return []
|
||||
|
||||
fifo_queue = [[x[0], getdate(x[1])] for x in fifo_queue]
|
||||
return fifo_queue
|
||||
|
||||
def __init_key_stores(self, row: dict) -> tuple:
|
||||
"Initialise keys and FIFO Queue."
|
||||
|
||||
key = (row.name, row.warehouse)
|
||||
self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
|
||||
fifo_queue = self.item_details[key]["fifo_queue"]
|
||||
if not row.name:
|
||||
key = (row.item_code, row.warehouse)
|
||||
else:
|
||||
key = (row.name, row.warehouse)
|
||||
|
||||
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
|
||||
self.transferred_item_details.setdefault(transferred_item_key, [])
|
||||
if key not in self.item_details:
|
||||
row.fifo_queue = self.format_fifo_queue(row.fifo_queue)
|
||||
|
||||
self.item_details.setdefault(key, {"details": row, "fifo_queue": row.fifo_queue or []})
|
||||
|
||||
fifo_queue = self.item_details[key]["fifo_queue"]
|
||||
transferred_item_key = None
|
||||
|
||||
if row.voucher_no:
|
||||
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
|
||||
self.transferred_item_details.setdefault(transferred_item_key, [])
|
||||
|
||||
return key, fifo_queue, transferred_item_key
|
||||
|
||||
@@ -351,10 +439,10 @@ class FIFOSlots:
|
||||
transfer_qty_to_pop = 0
|
||||
|
||||
def __update_balances(self, row: dict, key: tuple | str):
|
||||
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
|
||||
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or row.bal_qty
|
||||
|
||||
if "total_qty" not in self.item_details[key]:
|
||||
self.item_details[key]["total_qty"] = row.actual_qty
|
||||
self.item_details[key]["total_qty"] = row.actual_qty or row.bal_qty
|
||||
else:
|
||||
self.item_details[key]["total_qty"] += row.actual_qty
|
||||
|
||||
@@ -417,6 +505,10 @@ class FIFOSlots:
|
||||
)
|
||||
)
|
||||
|
||||
if self.start_from:
|
||||
from_date = get_datetime(get_date_str(self.start_from) + " 00:00:00")
|
||||
sle_query = sle_query.where(sle.posting_datetime >= from_date)
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
sle_query = self.__get_warehouse_conditions(sle, sle_query)
|
||||
elif self.filters.get("warehouse_type"):
|
||||
|
||||
@@ -1 +1 @@
|
||||
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||
{{ _("Powered by {0}").format('<a href="https://frappe.io/erpnext?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/frappe/erpnext.git"
|
||||
},
|
||||
"homepage": "https://erpnext.com",
|
||||
"homepage": "https://frappe.io/erpnext",
|
||||
"author": "Frappe Technologies Pvt. Ltd.",
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
|
||||
Reference in New Issue
Block a user