mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +00:00
Merge pull request #36546 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -238,7 +238,8 @@ class PaymentEntry(AccountsController):
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
@@ -635,7 +636,9 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
net_total = self.paid_amount
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -680,6 +683,20 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
doctype = "Sales Order"
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
|
||||
@@ -976,30 +976,6 @@ class PurchaseInvoice(BuyingController):
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||
)
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": self.supplier,
|
||||
"credit": precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_asset_gl_entry(self, gl_entries):
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
|
||||
@@ -1670,6 +1670,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = create_pr_against_po(po.name, received_qty=4)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -1075,6 +1075,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
|
||||
@@ -15,6 +15,7 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"Delivery Note": ["items", "delivery_note"],
|
||||
"Timesheet": ["timesheets", "time_sheet"],
|
||||
},
|
||||
"transactions": [
|
||||
|
||||
@@ -2049,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
)
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.account)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
@@ -2125,13 +2124,14 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.01, 0],
|
||||
["Round Off - _TC", 0.02, 0.01],
|
||||
]
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
@@ -3316,6 +3316,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
def test_sales_return_negative_rate(self):
|
||||
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
si.items[0].rate = 10
|
||||
si.save()
|
||||
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
|
||||
@@ -476,7 +476,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if (threshold and inv.tax_withholding_net_total >= threshold) or (
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||
):
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
|
||||
@@ -321,6 +321,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in reversed(orders):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
|
||||
|
||||
# Add some tax on the order
|
||||
order.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 8000,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
order.save()
|
||||
|
||||
order.apply_tds = 1
|
||||
order.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
order.submit()
|
||||
|
||||
self.assertEqual(order.taxes[0].tax_amount, 4000)
|
||||
|
||||
payment = get_payment_entry(order.doctype, order.name)
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
payment.submit()
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 4000)
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
|
||||
@@ -578,6 +614,7 @@ def create_records():
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
"Test TDS Supplier7",
|
||||
"Test TDS Supplier8",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import (
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
|
||||
):
|
||||
cond = "1=1"
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.party)
|
||||
.where(
|
||||
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
|
||||
)
|
||||
.groupby(gle.party)
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
query = query.select(Sum(gle.credit).as_("amount"))
|
||||
else:
|
||||
query = query.select(Sum(gle.debit).as_("amount"))
|
||||
|
||||
if posting_date:
|
||||
if future_payment:
|
||||
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
|
||||
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
|
||||
else:
|
||||
cond = "posting_date <= '{0}'".format(posting_date)
|
||||
query = query.where(gle.posting_date <= posting_date)
|
||||
|
||||
if company:
|
||||
cond += "and company = {0}".format(frappe.db.escape(company))
|
||||
query = query.where(gle.company == company)
|
||||
|
||||
if party:
|
||||
cond += "and party = {0}".format(frappe.db.escape(party))
|
||||
query = query.where(gle.party == party)
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" SELECT party, sum({0}) as amount
|
||||
FROM `tabGL Entry`
|
||||
WHERE
|
||||
party_type = %s and against_voucher is null
|
||||
and is_cancelled = 0
|
||||
and {1} GROUP BY party""".format(
|
||||
("credit") if party_type == "Customer" else "debit", cond
|
||||
),
|
||||
party_type,
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Supplier",
|
||||
"account_type": "Payable",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return ReceivablePayableReport(filters).run(args)
|
||||
|
||||
@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Supplier",
|
||||
"account_type": "Payable",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return AccountsReceivableSummary(filters).run(args)
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Customer",
|
||||
"account_type": "Receivable",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
return ReceivablePayableReport(filters).run(args)
|
||||
@@ -70,8 +70,11 @@ class ReceivablePayableReport(object):
|
||||
"Company", self.filters.get("company"), "default_currency"
|
||||
)
|
||||
self.currency_precision = get_currency_precision() or 2
|
||||
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
||||
self.party_type = self.filters.party_type
|
||||
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
|
||||
self.account_type = self.filters.account_type
|
||||
self.party_type = frappe.db.get_all(
|
||||
"Party Type", {"account_type": self.account_type}, pluck="name"
|
||||
)
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
@@ -197,6 +200,7 @@ class ReceivablePayableReport(object):
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
row.party_type = ple.party_type
|
||||
return row
|
||||
|
||||
def update_voucher_balance(self, ple):
|
||||
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_invoice_details(self):
|
||||
self.invoice_details = frappe._dict()
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
|
||||
d.sales_person
|
||||
)
|
||||
|
||||
if self.party_type == "Supplier":
|
||||
if self.account_type == "Payable":
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
@@ -421,8 +426,10 @@ class ReceivablePayableReport(object):
|
||||
# customer / supplier name
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
if self.filters.get(scrub(self.filters.party_type)):
|
||||
row.currency = row.account_currency
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
row.currency = row.account_currency
|
||||
break
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
|
||||
@@ -532,65 +539,67 @@ class ReceivablePayableReport(object):
|
||||
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
||||
|
||||
def get_future_payments_from_payment_entry(self):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
ref.reference_name as invoice_no,
|
||||
payment_entry.party,
|
||||
payment_entry.party_type,
|
||||
payment_entry.posting_date as future_date,
|
||||
ref.allocated_amount as future_amount,
|
||||
payment_entry.reference_no as future_ref
|
||||
from
|
||||
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
|
||||
on
|
||||
(ref.parent = payment_entry.name)
|
||||
where
|
||||
payment_entry.docstatus < 2
|
||||
and payment_entry.posting_date > %s
|
||||
and payment_entry.party_type = %s
|
||||
""",
|
||||
(self.filters.report_date, self.party_type),
|
||||
as_dict=1,
|
||||
)
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.inner_join(pe_ref)
|
||||
.on(pe_ref.parent == pe.name)
|
||||
.select(
|
||||
(pe_ref.reference_name).as_("invoice_no"),
|
||||
pe.party,
|
||||
pe.party_type,
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(pe.reference_no).as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus < 2)
|
||||
& (pe.posting_date > self.filters.report_date)
|
||||
& (pe.party_type.isin(self.party_type))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
def get_future_payments_from_journal_entry(self):
|
||||
if self.filters.get("party"):
|
||||
amount_field = (
|
||||
"jea.debit_in_account_currency - jea.credit_in_account_currency"
|
||||
if self.party_type == "Supplier"
|
||||
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
|
||||
)
|
||||
else:
|
||||
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
jea.reference_name as invoice_no,
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
query = (
|
||||
frappe.qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
jea.reference_name.as_("invoice_no"),
|
||||
jea.party,
|
||||
jea.party_type,
|
||||
je.posting_date as future_date,
|
||||
sum('{0}') as future_amount,
|
||||
je.cheque_no as future_ref
|
||||
from
|
||||
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
|
||||
on
|
||||
(jea.parent = je.name)
|
||||
where
|
||||
je.docstatus < 2
|
||||
and je.posting_date > %s
|
||||
and jea.party_type = %s
|
||||
and jea.reference_name is not null and jea.reference_name != ''
|
||||
group by je.name, jea.reference_name
|
||||
having future_amount > 0
|
||||
""".format(
|
||||
amount_field
|
||||
),
|
||||
(self.filters.report_date, self.party_type),
|
||||
as_dict=1,
|
||||
je.posting_date.as_("future_date"),
|
||||
je.cheque_no.as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus < 2)
|
||||
& (je.posting_date > self.filters.report_date)
|
||||
& (jea.party_type.isin(self.party_type))
|
||||
& (jea.reference_name.isnotnull())
|
||||
& (jea.reference_name != "")
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("party"):
|
||||
if self.account_type == "Payable":
|
||||
query = query.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
|
||||
)
|
||||
|
||||
query = query.having(qb.Field("future_amount") > 0)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def allocate_future_payments(self, row):
|
||||
# future payments are captured in additional columns
|
||||
# this method allocates pending future payments against a voucher to
|
||||
@@ -619,13 +628,17 @@ class ReceivablePayableReport(object):
|
||||
row.future_ref = ", ".join(row.future_ref)
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
or_filters = {}
|
||||
for party_type in self.party_type:
|
||||
party_field = scrub(party_type)
|
||||
if self.filters.get(party_field):
|
||||
or_filters.update({party_field: self.filters.get(party_field)})
|
||||
self.return_entries = frappe._dict(
|
||||
frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
|
||||
frappe.get_all(
|
||||
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
|
||||
)
|
||||
)
|
||||
|
||||
def set_ageing(self, row):
|
||||
@@ -716,6 +729,7 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.qb_selection_filter))
|
||||
.where(Criterion.any(self.or_filters))
|
||||
)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -746,16 +760,18 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def prepare_conditions(self):
|
||||
self.qb_selection_filter = []
|
||||
party_type_field = scrub(self.party_type)
|
||||
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
|
||||
self.or_filters = []
|
||||
for party_type in self.party_type:
|
||||
party_type_field = scrub(party_type)
|
||||
self.or_filters.append(self.ple.party_type == party_type)
|
||||
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters()
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters()
|
||||
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters()
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters()
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions()
|
||||
@@ -784,11 +800,10 @@ class ReceivablePayableReport(object):
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
else:
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
accounts = [
|
||||
d.name
|
||||
for d in frappe.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}
|
||||
"Account", filters={"account_type": self.account_type, "company": self.filters.company}
|
||||
)
|
||||
]
|
||||
|
||||
@@ -878,7 +893,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_party_details(self, party):
|
||||
if not party in self.party_details:
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
@@ -901,14 +916,20 @@ class ReceivablePayableReport(object):
|
||||
self.columns = []
|
||||
self.add_column("Posting Date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label=_(self.party_type),
|
||||
label="Party Type",
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label="Party",
|
||||
fieldname="party",
|
||||
fieldtype="Link",
|
||||
options=self.party_type,
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
self.add_column(
|
||||
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
|
||||
label=self.account_type + " Account",
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -916,13 +937,19 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
label = "Supplier Name"
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
label = "Customer Name"
|
||||
fieldname = "customer_name"
|
||||
self.add_column(
|
||||
_("{0} Name").format(self.party_type),
|
||||
fieldname=scrub(self.party_type) + "_name",
|
||||
label=label,
|
||||
fieldname=fieldname,
|
||||
fieldtype="Data",
|
||||
)
|
||||
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_column(
|
||||
_("Customer Contact"),
|
||||
fieldname="customer_primary_contact",
|
||||
@@ -942,7 +969,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
|
||||
if self.party_type == "Supplier":
|
||||
if self.account_type == "Payable":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
|
||||
|
||||
@@ -952,7 +979,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||
self.add_column(_("Paid Amount"), fieldname="paid")
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_column(_("Credit Note"), fieldname="credit_note")
|
||||
else:
|
||||
# note: fieldname is still `credit_note`
|
||||
@@ -970,7 +997,7 @@ class ReceivablePayableReport(object):
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
if self.filters.account_type == "Receivable":
|
||||
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
|
||||
|
||||
# comma separated list of linked delivery notes
|
||||
@@ -991,7 +1018,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
if self.filters.party_type == "Supplier":
|
||||
if self.filters.account_type == "Payable":
|
||||
self.add_column(
|
||||
label=_("Supplier Group"),
|
||||
fieldname="supplier_group",
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Customer",
|
||||
"account_type": "Receivable",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ def execute(filters=None):
|
||||
|
||||
class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def run(self, args):
|
||||
self.party_type = args.get("party_type")
|
||||
self.account_type = args.get("account_type")
|
||||
self.party_type = frappe.db.get_all(
|
||||
"Party Type", {"account_type": self.account_type}, pluck="name"
|
||||
)
|
||||
self.party_naming_by = frappe.db.get_value(
|
||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||
)
|
||||
@@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
self.get_party_total(args)
|
||||
|
||||
party = None
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
party = self.filters.get(scrub(party_type))
|
||||
|
||||
party_advance_amount = (
|
||||
get_partywise_advanced_payment_amount(
|
||||
self.party_type,
|
||||
self.filters.report_date,
|
||||
self.filters.show_future_payments,
|
||||
self.filters.company,
|
||||
party=self.filters.get(scrub(self.party_type)),
|
||||
party=party,
|
||||
account_type=self.account_type,
|
||||
)
|
||||
or {}
|
||||
)
|
||||
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
row.party = party
|
||||
if self.party_naming_by == "Naming Series":
|
||||
row.party_name = frappe.get_cached_value(
|
||||
self.party_type, party, scrub(self.party_type) + "_name"
|
||||
)
|
||||
if self.account_type == "Payable":
|
||||
doctype = "Supplier"
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
doctype = "Customer"
|
||||
fieldname = "customer_name"
|
||||
row.party_name = frappe.get_cached_value(doctype, party, fieldname)
|
||||
|
||||
row.update(party_dict)
|
||||
|
||||
@@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# set territory, customer_group, sales person etc
|
||||
self.set_party_details(d)
|
||||
self.party_total[d.party].update({"party_type": d.party_type})
|
||||
|
||||
def init_party_total(self, row):
|
||||
self.party_total.setdefault(
|
||||
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column(
|
||||
label=_(self.party_type),
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Link",
|
||||
options=self.party_type,
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
|
||||
self.add_column(
|
||||
label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
|
||||
fieldname="party_name",
|
||||
fieldtype="Data",
|
||||
)
|
||||
|
||||
credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
|
||||
credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
|
||||
|
||||
self.add_column(_("Advance Amount"), fieldname="advance")
|
||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||
@@ -159,7 +183,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_column(
|
||||
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
||||
)
|
||||
|
||||
51
erpnext/accounts/report/balance_sheet/test_balance_sheet.py
Normal file
51
erpnext/accounts/report/balance_sheet/test_balance_sheet.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.balance_sheet.balance_sheet import execute
|
||||
|
||||
|
||||
class TestBalanceSheet(FrappeTestCase):
|
||||
def test_balance_sheet(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
|
||||
create_sales_invoice,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company="_Test Company 6",
|
||||
warehouse="Finished Goods - _TC6",
|
||||
expense_account="Cost of Goods Sold - _TC6",
|
||||
cost_center="Main - _TC6",
|
||||
qty=10,
|
||||
rate=100,
|
||||
)
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company 6",
|
||||
debit_to="Debtors - _TC6",
|
||||
income_account="Sales - _TC6",
|
||||
cost_center="Main - _TC6",
|
||||
qty=5,
|
||||
rate=110,
|
||||
)
|
||||
filters = frappe._dict(
|
||||
company="_Test Company 6",
|
||||
period_start_date=today(),
|
||||
period_end_date=today(),
|
||||
periodicity="Yearly",
|
||||
)
|
||||
result = execute(filters)[1]
|
||||
for account_dict in result:
|
||||
if account_dict.get("account") == "Current Liabilities - _TC6":
|
||||
self.assertEqual(account_dict.total, 1000)
|
||||
if account_dict.get("account") == "Current Assets - _TC6":
|
||||
self.assertEqual(account_dict.total, 550)
|
||||
@@ -659,11 +659,12 @@ def set_gl_entries_by_account(
|
||||
& (gle.posting_date <= to_date)
|
||||
& (account.lft >= root_lft)
|
||||
& (account.rgt <= root_rgt)
|
||||
& (account.root_type <= root_type)
|
||||
)
|
||||
.orderby(gle.account, gle.posting_date)
|
||||
)
|
||||
|
||||
if root_type:
|
||||
query = query.where(account.root_type == root_type)
|
||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
|
||||
if additional_conditions:
|
||||
query = query.where(Criterion.all(additional_conditions))
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account_type: ['in', ["Receivable", "Payable"]]
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"voucher_no",
|
||||
"label": __("Voucher No"),
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["General and Payment Ledger Comparison"] = {
|
||||
"filters": get_filters()
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-08-02 17:30:29.494907",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-08-02 17:30:29.494907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General and Payment Ledger Comparison",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "General and Payment Ledger Comparison",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
class General_Payment_Ledger_Comparison(object):
|
||||
"""
|
||||
A Utility report to compare Voucher-wise balance between General and Payment Ledger
|
||||
"""
|
||||
|
||||
def __init__(self, filters=None):
|
||||
self.filters = filters
|
||||
self.gle = []
|
||||
self.ple = []
|
||||
|
||||
def get_accounts(self):
|
||||
receivable_accounts = [
|
||||
x[0]
|
||||
for x in frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": self.filters.company, "account_type": "Receivable"},
|
||||
as_list=True,
|
||||
)
|
||||
]
|
||||
payable_accounts = [
|
||||
x[0]
|
||||
for x in frappe.db.get_all(
|
||||
"Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True
|
||||
)
|
||||
]
|
||||
|
||||
self.account_types = frappe._dict(
|
||||
{
|
||||
"receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}),
|
||||
"payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}),
|
||||
}
|
||||
)
|
||||
|
||||
def generate_filters(self):
|
||||
if self.filters.account:
|
||||
self.account_types.receivable.accounts = []
|
||||
self.account_types.payable.accounts = []
|
||||
|
||||
for acc in frappe.db.get_all(
|
||||
"Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"]
|
||||
):
|
||||
if acc.account_type == "Receivable":
|
||||
self.account_types.receivable.accounts.append(acc.name)
|
||||
else:
|
||||
self.account_types.payable.accounts.append(acc.name)
|
||||
|
||||
def get_gle(self):
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
for acc_type, val in self.account_types.items():
|
||||
if val.accounts:
|
||||
|
||||
filter_criterion = []
|
||||
if self.filters.voucher_no:
|
||||
filter_criterion.append((gle.voucher_no == self.filters.voucher_no))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if acc_type == "receivable":
|
||||
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
|
||||
else:
|
||||
outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding")
|
||||
|
||||
self.account_types[acc_type].gle = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.company,
|
||||
gle.account,
|
||||
gle.voucher_no,
|
||||
gle.party,
|
||||
outstanding,
|
||||
)
|
||||
.where(
|
||||
(gle.company == self.filters.company)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
|
||||
.run()
|
||||
)
|
||||
|
||||
def get_ple(self):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
for acc_type, val in self.account_types.items():
|
||||
if val.accounts:
|
||||
|
||||
filter_criterion = []
|
||||
if self.filters.voucher_no:
|
||||
filter_criterion.append((ple.voucher_no == self.filters.voucher_no))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
self.account_types[acc_type].ple = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
|
||||
)
|
||||
.where(
|
||||
(ple.company == self.filters.company)
|
||||
& (ple.delinked == 0)
|
||||
& (ple.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
|
||||
.run()
|
||||
)
|
||||
|
||||
def compare(self):
|
||||
self.gle_balances = set()
|
||||
self.ple_balances = set()
|
||||
|
||||
# consolidate both receivable and payable balances in one set
|
||||
for acc_type, val in self.account_types.items():
|
||||
self.gle_balances = set(val.gle) | self.gle_balances
|
||||
self.ple_balances = set(val.ple) | self.ple_balances
|
||||
|
||||
self.diff1 = self.gle_balances.difference(self.ple_balances)
|
||||
self.diff2 = self.ple_balances.difference(self.gle_balances)
|
||||
self.diff = frappe._dict({})
|
||||
|
||||
for x in self.diff1:
|
||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||
|
||||
for x in self.diff2:
|
||||
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
|
||||
|
||||
def generate_data(self):
|
||||
self.data = []
|
||||
for key, val in self.diff.items():
|
||||
self.data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_no": key[2],
|
||||
"party": key[3],
|
||||
"gl_balance": val.gl_balance,
|
||||
"pl_balance": val.pl_balance,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher No"),
|
||||
fieldname="voucher_no",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("GL Balance"),
|
||||
fieldname="gl_balance",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Payment Ledger Balance"),
|
||||
fieldname="pl_balance",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.get_accounts()
|
||||
self.generate_filters()
|
||||
self.get_gle()
|
||||
self.get_ple()
|
||||
self.compare()
|
||||
self.generate_data()
|
||||
self.get_columns()
|
||||
|
||||
return self.columns, self.data
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
|
||||
rpt = General_Payment_Ledger_Comparison(filters)
|
||||
columns, data = rpt.run()
|
||||
|
||||
return columns, data
|
||||
@@ -0,0 +1,100 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def cleanup(self):
|
||||
doctypes = []
|
||||
doctypes.append(qb.DocType("GL Entry"))
|
||||
doctypes.append(qb.DocType("Payment Ledger Entry"))
|
||||
doctypes.append(qb.DocType("Sales Invoice"))
|
||||
|
||||
for doctype in doctypes:
|
||||
qb.from_(doctype).delete().where(doctype.company == self.company).run()
|
||||
|
||||
def test_01_basic_report_functionality(self):
|
||||
sinv = create_sales_invoice(
|
||||
company=self.company,
|
||||
debit_to=self.debit_to,
|
||||
expense_account=self.expense_account,
|
||||
cost_center=self.cost_center,
|
||||
income_account=self.income_account,
|
||||
warehouse=self.warehouse,
|
||||
)
|
||||
|
||||
# manually edit the payment ledger entry
|
||||
ple = frappe.db.get_all(
|
||||
"Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0}
|
||||
)[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1)
|
||||
|
||||
filters = frappe._dict({"company": self.company})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
|
||||
expected = {
|
||||
"voucher_no": sinv.name,
|
||||
"party": sinv.customer,
|
||||
"gl_balance": sinv.grand_total,
|
||||
"pl_balance": sinv.grand_total - 1,
|
||||
}
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
# account filter
|
||||
filters = frappe._dict({"company": self.company, "account": self.debit_to})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict({"company": self.company, "account": self.creditors})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
|
||||
# voucher_no filter
|
||||
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
|
||||
# date range filter
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"period_start_date": sinv.posting_date,
|
||||
"period_end_date": sinv.posting_date,
|
||||
}
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"period_start_date": add_days(sinv.posting_date, -1),
|
||||
"period_end_date": add_days(sinv.posting_date, -1),
|
||||
}
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
@@ -148,17 +148,33 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Selected Cost Center {} doesn't belongs to {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
if self.cost_center:
|
||||
cost_center_company, cost_center_is_group = frappe.db.get_value(
|
||||
"Cost Center", self.cost_center, ["company", "is_group"]
|
||||
)
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Cost Center {} doesn't belong to Company {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
if cost_center_is_group:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
|
||||
).format(frappe.bold(self.cost_center)),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
|
||||
else:
|
||||
if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
|
||||
).format(frappe.bold(self.company)),
|
||||
title=_("Missing Cost Center"),
|
||||
)
|
||||
|
||||
def validate_in_use_date(self):
|
||||
if not self.available_for_use_date:
|
||||
@@ -946,7 +962,9 @@ class Asset(AccountsController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_manual_depreciation_entries(self):
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(
|
||||
self.asset_category, self.company
|
||||
)
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -1185,10 +1203,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
|
||||
def make_journal_entry(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Max, Min
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
@@ -36,9 +38,40 @@ def post_depreciation_entries(date=None):
|
||||
failed_asset_names = []
|
||||
error_log_names = []
|
||||
|
||||
for asset_name in get_depreciable_assets(date):
|
||||
depreciable_assets = get_depreciable_assets(date)
|
||||
|
||||
credit_and_debit_accounts_for_asset_category_and_company = {}
|
||||
depreciation_cost_center_and_depreciation_series_for_company = (
|
||||
get_depreciation_cost_center_and_depreciation_series_for_company()
|
||||
)
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
for asset in depreciable_assets:
|
||||
asset_name, asset_category, asset_company, sch_start_idx, sch_end_idx = asset
|
||||
|
||||
if (
|
||||
asset_category,
|
||||
asset_company,
|
||||
) not in credit_and_debit_accounts_for_asset_category_and_company:
|
||||
credit_and_debit_accounts_for_asset_category_and_company.update(
|
||||
{
|
||||
(asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company(
|
||||
asset_category, asset_company
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
make_depreciation_entry(asset_name, date)
|
||||
make_depreciation_entry(
|
||||
asset_name,
|
||||
date,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
|
||||
depreciation_cost_center_and_depreciation_series_for_company[asset_company],
|
||||
accounting_dimensions,
|
||||
)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -54,115 +87,226 @@ def post_depreciation_entries(date=None):
|
||||
|
||||
|
||||
def get_depreciable_assets(date):
|
||||
return frappe.db.sql_list(
|
||||
"""select distinct a.name
|
||||
from tabAsset a, `tabDepreciation Schedule` ds
|
||||
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
|
||||
and a.status in ('Submitted', 'Partially Depreciated')
|
||||
and ifnull(ds.journal_entry, '')=''""",
|
||||
date,
|
||||
a = frappe.qb.DocType("Asset")
|
||||
ds = frappe.qb.DocType("Depreciation Schedule")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(a)
|
||||
.join(ds)
|
||||
.on(a.name == ds.parent)
|
||||
.select(a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
|
||||
.where(a.calculate_depreciation == 1)
|
||||
.where(a.docstatus == 1)
|
||||
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
|
||||
.where(ds.journal_entry.isnull())
|
||||
.where(ds.schedule_date <= date)
|
||||
.groupby(a.name)
|
||||
.orderby(a.creation, order=Order.desc)
|
||||
)
|
||||
|
||||
acc_frozen_upto = get_acc_frozen_upto()
|
||||
if acc_frozen_upto:
|
||||
res = res.where(ds.schedule_date > acc_frozen_upto)
|
||||
|
||||
res = res.run()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def get_acc_frozen_upto():
|
||||
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
if not acc_frozen_upto:
|
||||
return
|
||||
|
||||
frozen_accounts_modifier = frappe.db.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
|
||||
return getdate(acc_frozen_upto)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
|
||||
(
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset_category, company)
|
||||
|
||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
||||
accumulated_depreciation_account, depreciation_expense_account
|
||||
)
|
||||
|
||||
return (credit_account, debit_account)
|
||||
|
||||
|
||||
def get_depreciation_cost_center_and_depreciation_series_for_company():
|
||||
company_names = frappe.db.get_all("Company", pluck="name")
|
||||
|
||||
res = {}
|
||||
|
||||
for company_name in company_names:
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
res.update({company_name: (depreciation_cost_center, depreciation_series)})
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_depreciation_entry(asset_name, date=None):
|
||||
def make_depreciation_entry(
|
||||
asset_name,
|
||||
date=None,
|
||||
sch_start_idx=None,
|
||||
sch_end_idx=None,
|
||||
credit_and_debit_accounts=None,
|
||||
depreciation_cost_center_and_depreciation_series=None,
|
||||
accounting_dimensions=None,
|
||||
):
|
||||
frappe.has_permission("Journal Entry", throw=True)
|
||||
|
||||
if not date:
|
||||
date = today()
|
||||
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
if credit_and_debit_accounts:
|
||||
credit_account, debit_account = credit_and_debit_accounts
|
||||
else:
|
||||
credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
|
||||
if depreciation_cost_center_and_depreciation_series:
|
||||
depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
|
||||
else:
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
if not accounting_dimensions:
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
for d in asset.get("schedules"):
|
||||
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = d.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = d.finance_book
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
|
||||
depreciation_posting_error = None
|
||||
|
||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
||||
accumulated_depreciation_account, depreciation_expense_account
|
||||
for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]:
|
||||
try:
|
||||
_make_journal_entry_for_depreciation(
|
||||
asset,
|
||||
date,
|
||||
d,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
depreciation_cost_center,
|
||||
depreciation_series,
|
||||
credit_account,
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
)
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
|
||||
credit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
je.append("accounts", credit_entry)
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
|
||||
d.db_set("journal_entry", je.name)
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(d.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
depreciation_posting_error = e
|
||||
|
||||
asset.set_status()
|
||||
|
||||
return asset
|
||||
if not depreciation_posting_error:
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
return asset
|
||||
|
||||
raise depreciation_posting_error
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset):
|
||||
def _make_journal_entry_for_depreciation(
|
||||
asset,
|
||||
date,
|
||||
depr_schedule,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
depreciation_cost_center,
|
||||
depreciation_series,
|
||||
credit_account,
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
):
|
||||
if not (sch_start_idx and sch_end_idx) and not (
|
||||
not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date)
|
||||
):
|
||||
return
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = depr_schedule.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = depr_schedule.finance_book
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(
|
||||
asset.name, depr_schedule.depreciation_amount
|
||||
)
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": depr_schedule.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": depr_schedule.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
|
||||
credit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
je.append("accounts", credit_entry)
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
|
||||
depr_schedule.db_set("journal_entry", je.name)
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(depr_schedule.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= depr_schedule.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset_category, company):
|
||||
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||
|
||||
accounts = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": asset.asset_category, "company_name": asset.company},
|
||||
filters={"parent": asset_category, "company_name": company},
|
||||
fieldname=[
|
||||
"fixed_asset_account",
|
||||
"accumulated_depreciation_account",
|
||||
@@ -178,7 +322,7 @@ def get_depreciation_accounts(asset):
|
||||
|
||||
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||
accounts = frappe.get_cached_value(
|
||||
"Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
)
|
||||
|
||||
if not accumulated_depreciation_account:
|
||||
@@ -193,7 +337,7 @@ def get_depreciation_accounts(asset):
|
||||
):
|
||||
frappe.throw(
|
||||
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
||||
asset.asset_category, asset.company
|
||||
asset_category, company
|
||||
)
|
||||
)
|
||||
|
||||
@@ -533,8 +677,8 @@ def get_gl_entries_on_asset_disposal(
|
||||
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
|
||||
asset
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
@@ -50,10 +50,10 @@ class AssetValueAdjustment(Document):
|
||||
def make_depreciation_entry(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
(
|
||||
fixed_asset_account,
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
|
||||
@@ -369,7 +369,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "qty"]
|
||||
child_columns: ["item_code", "qty", "ordered_qty"]
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"col_break_email_1",
|
||||
"email_template",
|
||||
"preview",
|
||||
"send_attached_files",
|
||||
"sec_break_email_2",
|
||||
"message_for_supplier",
|
||||
"terms_section_break",
|
||||
@@ -285,13 +286,20 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, all files attached to this document will be attached to each email",
|
||||
"fieldname": "send_attached_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Attached Files"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-31 23:22:06.684694",
|
||||
"modified": "2023-07-27 16:41:48.468873",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -209,7 +209,9 @@ class RequestforQuotation(BuyingController):
|
||||
if preview:
|
||||
return message
|
||||
|
||||
attachments = self.get_attachments()
|
||||
attachments = None
|
||||
if self.send_attached_files:
|
||||
attachments = self.get_attachments()
|
||||
|
||||
self.send_email(data, sender, subject, message, attachments)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
apply_pricing_rule_on_transaction,
|
||||
get_applied_pricing_rules,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import (
|
||||
get_party_account,
|
||||
get_party_account_currency,
|
||||
@@ -1023,6 +1024,33 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||
)
|
||||
|
||||
credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
|
||||
against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": against,
|
||||
credit_or_debit: precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def update_against_document_in_jv(self):
|
||||
"""
|
||||
Links invoice and advance voucher:
|
||||
@@ -1672,8 +1700,13 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
self.append("payment_schedule", data)
|
||||
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not (
|
||||
automatically_fetch_payment_terms
|
||||
and allocate_payment_based_on_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
for d in self.get("payment_schedule"):
|
||||
|
||||
@@ -233,6 +233,9 @@ class StatusUpdater(Document):
|
||||
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
|
||||
|
||||
if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
|
||||
frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
|
||||
|
||||
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||
args["name"] = d.get(args["join_field"])
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ class SubcontractingController(StockController):
|
||||
"allow_zero_valuation": 1,
|
||||
}
|
||||
)
|
||||
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
|
||||
rm_obj.rate = get_incoming_rate(args)
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = qty
|
||||
|
||||
@@ -185,7 +185,7 @@ class Lead(SellingController, CRMNote):
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"job_title": self.job_title,
|
||||
"designation": self.job_title,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -432,7 +432,6 @@ scheduler_events = {
|
||||
"erpnext.controllers.accounts_controller.update_invoice_status",
|
||||
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
|
||||
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
|
||||
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||
"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
|
||||
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
|
||||
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
|
||||
@@ -459,6 +458,7 @@ scheduler_events = {
|
||||
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
|
||||
@@ -34,6 +34,7 @@ class LowerDeductionCertificate(Document):
|
||||
"supplier": self.supplier,
|
||||
"tax_withholding_category": self.tax_withholding_category,
|
||||
"name": ("!=", self.name),
|
||||
"company": self.company,
|
||||
},
|
||||
["name", "valid_from", "valid_upto"],
|
||||
as_dict=True,
|
||||
|
||||
@@ -6,12 +6,9 @@ import json
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from babel import Locale
|
||||
from frappe import _, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import formatdate, getdate, today
|
||||
from holidays import country_holidays
|
||||
from holidays.utils import list_supported_countries
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError):
|
||||
@@ -40,6 +37,8 @@ class HolidayList(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supported_countries(self):
|
||||
from holidays.utils import list_supported_countries
|
||||
|
||||
subdivisions_by_country = list_supported_countries()
|
||||
countries = [
|
||||
{"value": country, "label": local_country_name(country)}
|
||||
@@ -52,6 +51,8 @@ class HolidayList(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_local_holidays(self):
|
||||
from holidays import country_holidays
|
||||
|
||||
if not self.country:
|
||||
throw(_("Please select a country"))
|
||||
|
||||
@@ -169,4 +170,6 @@ def is_holiday(holiday_list, date=None):
|
||||
|
||||
def local_country_name(country_code: str) -> str:
|
||||
"""Return the localized country name for the given country code."""
|
||||
return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)
|
||||
from babel import Locale
|
||||
|
||||
return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code)
|
||||
|
||||
@@ -8,6 +8,8 @@ from datetime import date, timedelta
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
|
||||
|
||||
|
||||
class TestHolidayList(unittest.TestCase):
|
||||
def test_holiday_list(self):
|
||||
@@ -58,6 +60,16 @@ class TestHolidayList(unittest.TestCase):
|
||||
self.assertIn(date(2023, 4, 10), holidays)
|
||||
self.assertNotIn(date(2023, 5, 1), holidays)
|
||||
|
||||
def test_localized_country_names(self):
|
||||
lang = frappe.local.lang
|
||||
frappe.local.lang = "en-gb"
|
||||
self.assertEqual(local_country_name("IN"), "India")
|
||||
self.assertEqual(local_country_name("DE"), "Germany")
|
||||
|
||||
frappe.local.lang = "de"
|
||||
self.assertEqual(local_country_name("DE"), "Deutschland")
|
||||
frappe.local.lang = lang
|
||||
|
||||
|
||||
def make_holiday_list(
|
||||
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
|
||||
|
||||
@@ -11,6 +11,7 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "against_sales_order"],
|
||||
"Sales Invoice": ["items", "against_sales_invoice"],
|
||||
"Material Request": ["items", "material_request"],
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
},
|
||||
|
||||
@@ -228,7 +228,8 @@ class MaterialRequest(BuyingController):
|
||||
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||
|
||||
if mr_qty_allowance:
|
||||
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
|
||||
allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty"))
|
||||
|
||||
if d.ordered_qty and d.ordered_qty > allowed_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
|
||||
@@ -194,7 +194,7 @@ class StockEntry(StockController):
|
||||
return False
|
||||
|
||||
# If line items are more than 100 or record is older than 6 months
|
||||
if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6:
|
||||
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -607,7 +607,7 @@ class StockReconciliation(StockController):
|
||||
)
|
||||
|
||||
if sl_entries:
|
||||
self.make_sl_entries(sl_entries)
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=True)
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
|
||||
@@ -594,6 +594,67 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
|
||||
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
|
||||
|
||||
def test_subcontracting_receipt_raw_material_rate(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
# Step - 1: Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
# Step - 2: Create FG and RM Items
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||
rm_item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
# Step - 3: Create BOM for FG Item
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||
for rm_item in bom.items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
bom = bom.name
|
||||
|
||||
# Step - 4: Create PO and SCO
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 100,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 100,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
for rm_item in sco.supplied_items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
|
||||
# Step - 5: Inward Raw Materials
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
for rm_item in rm_items:
|
||||
rm_item["rate"] = 100
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
|
||||
# Step - 6: Transfer RM's to Subcontractor
|
||||
se = make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
for item in se.items:
|
||||
self.assertEqual(item.qty, 100)
|
||||
self.assertEqual(item.basic_rate, 100)
|
||||
self.assertEqual(item.amount, item.qty * item.basic_rate)
|
||||
|
||||
# Step - 7: Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.submit()
|
||||
scr.load_from_db()
|
||||
for rm_item in scr.supplied_items:
|
||||
self.assertEqual(rm_item.consumed_qty, 100)
|
||||
self.assertEqual(rm_item.rate, 100)
|
||||
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
|
||||
|
||||
|
||||
def make_return_subcontracting_receipt(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
Reference in New Issue
Block a user