mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 22:49:19 +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
|
d.payment_term
|
||||||
and (
|
and (
|
||||||
(flt(d.allocated_amount)) > 0
|
(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)
|
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:
|
if not self.apply_tax_withholding_amount:
|
||||||
return
|
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
|
# Adding args as purchase invoice to get TDS amount
|
||||||
args = frappe._dict(
|
args = frappe._dict(
|
||||||
@@ -680,6 +683,20 @@ class PaymentEntry(AccountsController):
|
|||||||
for d in to_remove:
|
for d in to_remove:
|
||||||
self.remove(d)
|
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):
|
def apply_taxes(self):
|
||||||
self.initialize_taxes()
|
self.initialize_taxes()
|
||||||
self.determine_exclusive_rate()
|
self.determine_exclusive_rate()
|
||||||
|
|||||||
@@ -976,30 +976,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.item_tax_amount, item.precision("item_tax_amount")
|
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):
|
def get_asset_gl_entry(self, gl_entries):
|
||||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
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)
|
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
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):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
|
|||||||
@@ -1075,6 +1075,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.make_internal_transfer_gl_entries(gl_entries)
|
self.make_internal_transfer_gl_entries(gl_entries)
|
||||||
|
|
||||||
self.make_item_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)
|
self.make_discount_gl_entries(gl_entries)
|
||||||
|
|
||||||
# merge gl entries before adding pos entries
|
# merge gl entries before adding pos entries
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ def get_data():
|
|||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "sales_order"],
|
"Sales Order": ["items", "sales_order"],
|
||||||
|
"Delivery Note": ["items", "delivery_note"],
|
||||||
"Timesheet": ["timesheets", "time_sheet"],
|
"Timesheet": ["timesheets", "time_sheet"],
|
||||||
},
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
|
|||||||
@@ -2049,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||||
|
|
||||||
expected_values = dict(
|
expected_values = [
|
||||||
(d[0], d)
|
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||||
for d in [
|
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||||
[si.debit_to, 1500, 0.0],
|
[si.debit_to, 1500, 0.0],
|
||||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
["Round Off - _TC", 0.01, 0.01],
|
||||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
["Sales - _TC", 0.0, 1271.18],
|
||||||
["Sales - _TC", 0.0, 1271.18],
|
]
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
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
|
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||||
|
group by account
|
||||||
order by account asc""",
|
order by account asc""",
|
||||||
si.name,
|
si.name,
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
for gle in gl_entries:
|
for i, gle in enumerate(gl_entries):
|
||||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
self.assertEqual(expected_values[i][0], gle.account)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
self.assertEqual(expected_values[i][1], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
self.assertEqual(expected_values[i][2], gle.credit)
|
||||||
|
|
||||||
def test_rounding_adjustment_3(self):
|
def test_rounding_adjustment_3(self):
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
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 Service Tax - _TC", 0.0, 240.43],
|
||||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||||
["Sales - _TC", 0.0, 4007.15],
|
["Sales - _TC", 0.0, 4007.15],
|
||||||
["Round Off - _TC", 0.01, 0],
|
["Round Off - _TC", 0.02, 0.01],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
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
|
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||||
|
group by account
|
||||||
order by account asc""",
|
order by account asc""",
|
||||||
si.name,
|
si.name,
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
@@ -3316,6 +3316,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertRaises(frappe.ValidationError, si.submit)
|
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():
|
def get_sales_invoice_for_e_invoice():
|
||||||
si = make_sales_invoice_for_ewaybill()
|
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)
|
threshold = tax_details.get("threshold", 0)
|
||||||
cumulative_threshold = tax_details.get("cumulative_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
|
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||||
):
|
):
|
||||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||||
|
|||||||
@@ -321,6 +321,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
for d in reversed(orders):
|
for d in reversed(orders):
|
||||||
d.cancel()
|
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):
|
def test_multi_category_single_supplier(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
|
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
|
||||||
@@ -578,6 +614,7 @@ def create_records():
|
|||||||
"Test TDS Supplier5",
|
"Test TDS Supplier5",
|
||||||
"Test TDS Supplier6",
|
"Test TDS Supplier6",
|
||||||
"Test TDS Supplier7",
|
"Test TDS Supplier7",
|
||||||
|
"Test TDS Supplier8",
|
||||||
]:
|
]:
|
||||||
if frappe.db.exists("Supplier", name):
|
if frappe.db.exists("Supplier", name):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import (
|
|||||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
|
from frappe.query_builder.functions import Date, Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
@@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def get_partywise_advanced_payment_amount(
|
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 posting_date:
|
||||||
if future_payment:
|
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:
|
else:
|
||||||
cond = "posting_date <= '{0}'".format(posting_date)
|
query = query.where(gle.posting_date <= posting_date)
|
||||||
|
|
||||||
if company:
|
if company:
|
||||||
cond += "and company = {0}".format(frappe.db.escape(company))
|
query = query.where(gle.company == company)
|
||||||
|
|
||||||
if party:
|
if party:
|
||||||
cond += "and party = {0}".format(frappe.db.escape(party))
|
query = query.where(gle.party == party)
|
||||||
|
|
||||||
data = frappe.db.sql(
|
data = query.run(as_dict=True)
|
||||||
""" 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,
|
|
||||||
)
|
|
||||||
if data:
|
if data:
|
||||||
return frappe._dict(data)
|
return frappe._dict(data)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Supplier",
|
"account_type": "Payable",
|
||||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||||
}
|
}
|
||||||
return ReceivablePayableReport(filters).run(args)
|
return ReceivablePayableReport(filters).run(args)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Supplier",
|
"account_type": "Payable",
|
||||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||||
}
|
}
|
||||||
return AccountsReceivableSummary(filters).run(args)
|
return AccountsReceivableSummary(filters).run(args)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, scrub
|
from frappe import _, qb, scrub
|
||||||
from frappe.query_builder import Criterion
|
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 frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
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):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Customer",
|
"account_type": "Receivable",
|
||||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||||
}
|
}
|
||||||
return ReceivablePayableReport(filters).run(args)
|
return ReceivablePayableReport(filters).run(args)
|
||||||
@@ -70,8 +70,11 @@ class ReceivablePayableReport(object):
|
|||||||
"Company", self.filters.get("company"), "default_currency"
|
"Company", self.filters.get("company"), "default_currency"
|
||||||
)
|
)
|
||||||
self.currency_precision = get_currency_precision() or 2
|
self.currency_precision = get_currency_precision() or 2
|
||||||
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
|
||||||
self.party_type = self.filters.party_type
|
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.party_details = {}
|
||||||
self.invoices = set()
|
self.invoices = set()
|
||||||
self.skip_total_row = 0
|
self.skip_total_row = 0
|
||||||
@@ -197,6 +200,7 @@ class ReceivablePayableReport(object):
|
|||||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
# 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 = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||||
|
|
||||||
|
row.party_type = ple.party_type
|
||||||
return row
|
return row
|
||||||
|
|
||||||
def update_voucher_balance(self, ple):
|
def update_voucher_balance(self, ple):
|
||||||
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||||
if self.filters.get(scrub(self.party_type)):
|
for party_type in self.party_type:
|
||||||
amount = ple.amount_in_account_currency
|
if self.filters.get(scrub(party_type)):
|
||||||
|
amount = ple.amount_in_account_currency
|
||||||
else:
|
else:
|
||||||
amount = ple.amount
|
amount = ple.amount
|
||||||
amount_in_account_currency = ple.amount_in_account_currency
|
amount_in_account_currency = ple.amount_in_account_currency
|
||||||
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def get_invoice_details(self):
|
def get_invoice_details(self):
|
||||||
self.invoice_details = frappe._dict()
|
self.invoice_details = frappe._dict()
|
||||||
if self.party_type == "Customer":
|
if self.account_type == "Receivable":
|
||||||
si_list = frappe.db.sql(
|
si_list = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select name, due_date, po_no
|
select name, due_date, po_no
|
||||||
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
|
|||||||
d.sales_person
|
d.sales_person
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.party_type == "Supplier":
|
if self.account_type == "Payable":
|
||||||
for pi in frappe.db.sql(
|
for pi in frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select name, due_date, bill_no, bill_date
|
select name, due_date, bill_no, bill_date
|
||||||
@@ -421,8 +426,10 @@ class ReceivablePayableReport(object):
|
|||||||
# customer / supplier name
|
# customer / supplier name
|
||||||
party_details = self.get_party_details(row.party) or {}
|
party_details = self.get_party_details(row.party) or {}
|
||||||
row.update(party_details)
|
row.update(party_details)
|
||||||
if self.filters.get(scrub(self.filters.party_type)):
|
for party_type in self.party_type:
|
||||||
row.currency = row.account_currency
|
if self.filters.get(scrub(party_type)):
|
||||||
|
row.currency = row.account_currency
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
row.currency = self.company_currency
|
row.currency = self.company_currency
|
||||||
|
|
||||||
@@ -532,65 +539,67 @@ class ReceivablePayableReport(object):
|
|||||||
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
||||||
|
|
||||||
def get_future_payments_from_payment_entry(self):
|
def get_future_payments_from_payment_entry(self):
|
||||||
return frappe.db.sql(
|
pe = frappe.qb.DocType("Payment Entry")
|
||||||
"""
|
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||||
select
|
return (
|
||||||
ref.reference_name as invoice_no,
|
frappe.qb.from_(pe)
|
||||||
payment_entry.party,
|
.inner_join(pe_ref)
|
||||||
payment_entry.party_type,
|
.on(pe_ref.parent == pe.name)
|
||||||
payment_entry.posting_date as future_date,
|
.select(
|
||||||
ref.allocated_amount as future_amount,
|
(pe_ref.reference_name).as_("invoice_no"),
|
||||||
payment_entry.reference_no as future_ref
|
pe.party,
|
||||||
from
|
pe.party_type,
|
||||||
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
|
(pe.posting_date).as_("future_date"),
|
||||||
on
|
(pe_ref.allocated_amount).as_("future_amount"),
|
||||||
(ref.parent = payment_entry.name)
|
(pe.reference_no).as_("future_ref"),
|
||||||
where
|
)
|
||||||
payment_entry.docstatus < 2
|
.where(
|
||||||
and payment_entry.posting_date > %s
|
(pe.docstatus < 2)
|
||||||
and payment_entry.party_type = %s
|
& (pe.posting_date > self.filters.report_date)
|
||||||
""",
|
& (pe.party_type.isin(self.party_type))
|
||||||
(self.filters.report_date, self.party_type),
|
)
|
||||||
as_dict=1,
|
).run(as_dict=True)
|
||||||
)
|
|
||||||
|
|
||||||
def get_future_payments_from_journal_entry(self):
|
def get_future_payments_from_journal_entry(self):
|
||||||
if self.filters.get("party"):
|
je = frappe.qb.DocType("Journal Entry")
|
||||||
amount_field = (
|
jea = frappe.qb.DocType("Journal Entry Account")
|
||||||
"jea.debit_in_account_currency - jea.credit_in_account_currency"
|
query = (
|
||||||
if self.party_type == "Supplier"
|
frappe.qb.from_(je)
|
||||||
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
|
.inner_join(jea)
|
||||||
)
|
.on(jea.parent == je.name)
|
||||||
else:
|
.select(
|
||||||
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
|
jea.reference_name.as_("invoice_no"),
|
||||||
|
|
||||||
return frappe.db.sql(
|
|
||||||
"""
|
|
||||||
select
|
|
||||||
jea.reference_name as invoice_no,
|
|
||||||
jea.party,
|
jea.party,
|
||||||
jea.party_type,
|
jea.party_type,
|
||||||
je.posting_date as future_date,
|
je.posting_date.as_("future_date"),
|
||||||
sum('{0}') as future_amount,
|
je.cheque_no.as_("future_ref"),
|
||||||
je.cheque_no as future_ref
|
)
|
||||||
from
|
.where(
|
||||||
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
|
(je.docstatus < 2)
|
||||||
on
|
& (je.posting_date > self.filters.report_date)
|
||||||
(jea.parent = je.name)
|
& (jea.party_type.isin(self.party_type))
|
||||||
where
|
& (jea.reference_name.isnotnull())
|
||||||
je.docstatus < 2
|
& (jea.reference_name != "")
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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):
|
def allocate_future_payments(self, row):
|
||||||
# future payments are captured in additional columns
|
# future payments are captured in additional columns
|
||||||
# this method allocates pending future payments against a voucher to
|
# this method allocates pending future payments against a voucher to
|
||||||
@@ -619,13 +628,17 @@ class ReceivablePayableReport(object):
|
|||||||
row.future_ref = ", ".join(row.future_ref)
|
row.future_ref = ", ".join(row.future_ref)
|
||||||
|
|
||||||
def get_return_entries(self):
|
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}
|
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||||
party_field = scrub(self.filters.party_type)
|
or_filters = {}
|
||||||
if self.filters.get(party_field):
|
for party_type in self.party_type:
|
||||||
filters.update({party_field: self.filters.get(party_field)})
|
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(
|
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):
|
def set_ageing(self, row):
|
||||||
@@ -716,6 +729,7 @@ class ReceivablePayableReport(object):
|
|||||||
)
|
)
|
||||||
.where(ple.delinked == 0)
|
.where(ple.delinked == 0)
|
||||||
.where(Criterion.all(self.qb_selection_filter))
|
.where(Criterion.all(self.qb_selection_filter))
|
||||||
|
.where(Criterion.any(self.or_filters))
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.filters.get("group_by_party"):
|
if self.filters.get("group_by_party"):
|
||||||
@@ -746,16 +760,18 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def prepare_conditions(self):
|
def prepare_conditions(self):
|
||||||
self.qb_selection_filter = []
|
self.qb_selection_filter = []
|
||||||
party_type_field = scrub(self.party_type)
|
self.or_filters = []
|
||||||
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
|
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":
|
if party_type_field == "customer":
|
||||||
self.add_customer_filters()
|
self.add_customer_filters()
|
||||||
|
|
||||||
elif party_type_field == "supplier":
|
elif party_type_field == "supplier":
|
||||||
self.add_supplier_filters()
|
self.add_supplier_filters()
|
||||||
|
|
||||||
if self.filters.cost_center:
|
if self.filters.cost_center:
|
||||||
self.get_cost_center_conditions()
|
self.get_cost_center_conditions()
|
||||||
@@ -784,11 +800,10 @@ class ReceivablePayableReport(object):
|
|||||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||||
else:
|
else:
|
||||||
# get GL with "receivable" or "payable" account_type
|
# get GL with "receivable" or "payable" account_type
|
||||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
|
||||||
accounts = [
|
accounts = [
|
||||||
d.name
|
d.name
|
||||||
for d in frappe.get_all(
|
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):
|
def get_party_details(self, party):
|
||||||
if not party in self.party_details:
|
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"]
|
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||||
|
|
||||||
if self.filters.get("sales_partner"):
|
if self.filters.get("sales_partner"):
|
||||||
@@ -901,14 +916,20 @@ class ReceivablePayableReport(object):
|
|||||||
self.columns = []
|
self.columns = []
|
||||||
self.add_column("Posting Date", fieldtype="Date")
|
self.add_column("Posting Date", fieldtype="Date")
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label=_(self.party_type),
|
label="Party Type",
|
||||||
|
fieldname="party_type",
|
||||||
|
fieldtype="Data",
|
||||||
|
width=100,
|
||||||
|
)
|
||||||
|
self.add_column(
|
||||||
|
label="Party",
|
||||||
fieldname="party",
|
fieldname="party",
|
||||||
fieldtype="Link",
|
fieldtype="Dynamic Link",
|
||||||
options=self.party_type,
|
options="party_type",
|
||||||
width=180,
|
width=180,
|
||||||
)
|
)
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
|
label=self.account_type + " Account",
|
||||||
fieldname="party_account",
|
fieldname="party_account",
|
||||||
fieldtype="Link",
|
fieldtype="Link",
|
||||||
options="Account",
|
options="Account",
|
||||||
@@ -916,13 +937,19 @@ class ReceivablePayableReport(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.party_naming_by == "Naming Series":
|
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(
|
self.add_column(
|
||||||
_("{0} Name").format(self.party_type),
|
label=label,
|
||||||
fieldname=scrub(self.party_type) + "_name",
|
fieldname=fieldname,
|
||||||
fieldtype="Data",
|
fieldtype="Data",
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.party_type == "Customer":
|
if self.account_type == "Receivable":
|
||||||
self.add_column(
|
self.add_column(
|
||||||
_("Customer Contact"),
|
_("Customer Contact"),
|
||||||
fieldname="customer_primary_contact",
|
fieldname="customer_primary_contact",
|
||||||
@@ -942,7 +969,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
self.add_column(label="Due Date", fieldtype="Date")
|
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 No"), fieldname="bill_no", fieldtype="Data")
|
||||||
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
|
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(_("Invoiced Amount"), fieldname="invoiced")
|
||||||
self.add_column(_("Paid Amount"), fieldname="paid")
|
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")
|
self.add_column(_("Credit Note"), fieldname="credit_note")
|
||||||
else:
|
else:
|
||||||
# note: fieldname is still `credit_note`
|
# 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=_("Future Payment Amount"), fieldname="future_amount")
|
||||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
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")
|
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
|
||||||
|
|
||||||
# comma separated list of linked delivery notes
|
# comma separated list of linked delivery notes
|
||||||
@@ -991,7 +1018,7 @@ class ReceivablePayableReport(object):
|
|||||||
if self.filters.sales_partner:
|
if self.filters.sales_partner:
|
||||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
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(
|
self.add_column(
|
||||||
label=_("Supplier Group"),
|
label=_("Supplier Group"),
|
||||||
fieldname="supplier_group",
|
fieldname="supplier_group",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
args = {
|
args = {
|
||||||
"party_type": "Customer",
|
"account_type": "Receivable",
|
||||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,10 @@ def execute(filters=None):
|
|||||||
|
|
||||||
class AccountsReceivableSummary(ReceivablePayableReport):
|
class AccountsReceivableSummary(ReceivablePayableReport):
|
||||||
def run(self, args):
|
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(
|
self.party_naming_by = frappe.db.get_value(
|
||||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||||
)
|
)
|
||||||
@@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
self.get_party_total(args)
|
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 = (
|
party_advance_amount = (
|
||||||
get_partywise_advanced_payment_amount(
|
get_partywise_advanced_payment_amount(
|
||||||
self.party_type,
|
self.party_type,
|
||||||
self.filters.report_date,
|
self.filters.report_date,
|
||||||
self.filters.show_future_payments,
|
self.filters.show_future_payments,
|
||||||
self.filters.company,
|
self.filters.company,
|
||||||
party=self.filters.get(scrub(self.party_type)),
|
party=party,
|
||||||
|
account_type=self.account_type,
|
||||||
)
|
)
|
||||||
or {}
|
or {}
|
||||||
)
|
)
|
||||||
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
row.party = party
|
row.party = party
|
||||||
if self.party_naming_by == "Naming Series":
|
if self.party_naming_by == "Naming Series":
|
||||||
row.party_name = frappe.get_cached_value(
|
if self.account_type == "Payable":
|
||||||
self.party_type, party, scrub(self.party_type) + "_name"
|
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)
|
row.update(party_dict)
|
||||||
|
|
||||||
@@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
# set territory, customer_group, sales person etc
|
# set territory, customer_group, sales person etc
|
||||||
self.set_party_details(d)
|
self.set_party_details(d)
|
||||||
|
self.party_total[d.party].update({"party_type": d.party_type})
|
||||||
|
|
||||||
def init_party_total(self, row):
|
def init_party_total(self, row):
|
||||||
self.party_total.setdefault(
|
self.party_total.setdefault(
|
||||||
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
self.columns = []
|
self.columns = []
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label=_(self.party_type),
|
label=_("Party Type"),
|
||||||
|
fieldname="party_type",
|
||||||
|
fieldtype="Data",
|
||||||
|
width=100,
|
||||||
|
)
|
||||||
|
self.add_column(
|
||||||
|
label=_("Party"),
|
||||||
fieldname="party",
|
fieldname="party",
|
||||||
fieldtype="Link",
|
fieldtype="Dynamic Link",
|
||||||
options=self.party_type,
|
options="party_type",
|
||||||
width=180,
|
width=180,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.party_naming_by == "Naming Series":
|
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(_("Advance Amount"), fieldname="advance")
|
||||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
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=_("Future Payment Amount"), fieldname="future_amount")
|
||||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||||
|
|
||||||
if self.party_type == "Customer":
|
if self.account_type == "Receivable":
|
||||||
self.add_column(
|
self.add_column(
|
||||||
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
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)
|
& (gle.posting_date <= to_date)
|
||||||
& (account.lft >= root_lft)
|
& (account.lft >= root_lft)
|
||||||
& (account.rgt <= root_rgt)
|
& (account.rgt <= root_rgt)
|
||||||
& (account.root_type <= root_type)
|
|
||||||
)
|
)
|
||||||
.orderby(gle.account, gle.posting_date)
|
.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)
|
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
|
||||||
if additional_conditions:
|
if additional_conditions:
|
||||||
query = query.where(Criterion.all(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))
|
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
||||||
|
|
||||||
def validate_cost_center(self):
|
def validate_cost_center(self):
|
||||||
if not self.cost_center:
|
if self.cost_center:
|
||||||
return
|
cost_center_company, cost_center_is_group = frappe.db.get_value(
|
||||||
|
"Cost Center", self.cost_center, ["company", "is_group"]
|
||||||
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 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):
|
def validate_in_use_date(self):
|
||||||
if not self.available_for_use_date:
|
if not self.available_for_use_date:
|
||||||
@@ -946,7 +962,9 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_manual_depreciation_entries(self):
|
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")
|
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):
|
def make_journal_entry(asset_name):
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
(
|
(
|
||||||
fixed_asset_account,
|
_,
|
||||||
accumulated_depreciation_account,
|
accumulated_depreciation_account,
|
||||||
depreciation_expense_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(
|
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder import Order
|
||||||
|
from frappe.query_builder.functions import Max, Min
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
@@ -36,9 +38,40 @@ def post_depreciation_entries(date=None):
|
|||||||
failed_asset_names = []
|
failed_asset_names = []
|
||||||
error_log_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:
|
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()
|
frappe.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@@ -54,115 +87,226 @@ def post_depreciation_entries(date=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_depreciable_assets(date):
|
def get_depreciable_assets(date):
|
||||||
return frappe.db.sql_list(
|
a = frappe.qb.DocType("Asset")
|
||||||
"""select distinct a.name
|
ds = frappe.qb.DocType("Depreciation Schedule")
|
||||||
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
|
res = (
|
||||||
and a.status in ('Submitted', 'Partially Depreciated')
|
frappe.qb.from_(a)
|
||||||
and ifnull(ds.journal_entry, '')=''""",
|
.join(ds)
|
||||||
date,
|
.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()
|
@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)
|
frappe.has_permission("Journal Entry", throw=True)
|
||||||
|
|
||||||
if not date:
|
if not date:
|
||||||
date = today()
|
date = today()
|
||||||
|
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
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(
|
if credit_and_debit_accounts:
|
||||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
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
|
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"):
|
depreciation_posting_error = None
|
||||||
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)
|
|
||||||
|
|
||||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]:
|
||||||
accumulated_depreciation_account, depreciation_expense_account
|
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,
|
||||||
)
|
)
|
||||||
|
frappe.db.commit()
|
||||||
credit_entry = {
|
except Exception as e:
|
||||||
"account": credit_account,
|
frappe.db.rollback()
|
||||||
"credit_in_account_currency": d.depreciation_amount,
|
depreciation_posting_error = e
|
||||||
"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")
|
|
||||||
|
|
||||||
asset.set_status()
|
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
|
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||||
|
|
||||||
accounts = frappe.db.get_value(
|
accounts = frappe.db.get_value(
|
||||||
"Asset Category Account",
|
"Asset Category Account",
|
||||||
filters={"parent": asset.asset_category, "company_name": asset.company},
|
filters={"parent": asset_category, "company_name": company},
|
||||||
fieldname=[
|
fieldname=[
|
||||||
"fixed_asset_account",
|
"fixed_asset_account",
|
||||||
"accumulated_depreciation_account",
|
"accumulated_depreciation_account",
|
||||||
@@ -178,7 +322,7 @@ def get_depreciation_accounts(asset):
|
|||||||
|
|
||||||
if not accumulated_depreciation_account or not depreciation_expense_account:
|
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||||
accounts = frappe.get_cached_value(
|
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:
|
if not accumulated_depreciation_account:
|
||||||
@@ -193,7 +337,7 @@ def get_depreciation_accounts(asset):
|
|||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
_("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):
|
def get_asset_details(asset, finance_book=None):
|
||||||
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
|
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||||
asset
|
asset.asset_category, asset.company
|
||||||
)
|
)
|
||||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(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
|
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ class AssetValueAdjustment(Document):
|
|||||||
def make_depreciation_entry(self):
|
def make_depreciation_entry(self):
|
||||||
asset = frappe.get_doc("Asset", self.asset)
|
asset = frappe.get_doc("Asset", self.asset)
|
||||||
(
|
(
|
||||||
fixed_asset_account,
|
_,
|
||||||
accumulated_depreciation_account,
|
accumulated_depreciation_account,
|
||||||
depreciation_expense_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(
|
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
"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,
|
allow_child_item_selection: true,
|
||||||
child_fieldname: "items",
|
child_fieldname: "items",
|
||||||
child_columns: ["item_code", "qty"]
|
child_columns: ["item_code", "qty", "ordered_qty"]
|
||||||
})
|
})
|
||||||
}, __("Get Items From"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"col_break_email_1",
|
"col_break_email_1",
|
||||||
"email_template",
|
"email_template",
|
||||||
"preview",
|
"preview",
|
||||||
|
"send_attached_files",
|
||||||
"sec_break_email_2",
|
"sec_break_email_2",
|
||||||
"message_for_supplier",
|
"message_for_supplier",
|
||||||
"terms_section_break",
|
"terms_section_break",
|
||||||
@@ -285,13 +286,20 @@
|
|||||||
"fieldname": "named_place",
|
"fieldname": "named_place",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Named Place"
|
"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",
|
"icon": "fa fa-shopping-cart",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-31 23:22:06.684694",
|
"modified": "2023-07-27 16:41:48.468873",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ class RequestforQuotation(BuyingController):
|
|||||||
if preview:
|
if preview:
|
||||||
return message
|
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)
|
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,
|
apply_pricing_rule_on_transaction,
|
||||||
get_applied_pricing_rules,
|
get_applied_pricing_rules,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||||
from erpnext.accounts.party import (
|
from erpnext.accounts.party import (
|
||||||
get_party_account,
|
get_party_account,
|
||||||
get_party_account_currency,
|
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):
|
def update_against_document_in_jv(self):
|
||||||
"""
|
"""
|
||||||
Links invoice and advance voucher:
|
Links invoice and advance voucher:
|
||||||
@@ -1672,8 +1700,13 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
self.append("payment_schedule", data)
|
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 (
|
if not (
|
||||||
automatically_fetch_payment_terms
|
automatically_fetch_payment_terms
|
||||||
|
and allocate_payment_based_on_payment_terms
|
||||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||||
):
|
):
|
||||||
for d in self.get("payment_schedule"):
|
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"):
|
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))
|
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"]):
|
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||||
args["name"] = d.get(args["join_field"])
|
args["name"] = d.get(args["join_field"])
|
||||||
|
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ class SubcontractingController(StockController):
|
|||||||
"allow_zero_valuation": 1,
|
"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:
|
if self.doctype == self.subcontract_data.order_doctype:
|
||||||
rm_obj.required_qty = qty
|
rm_obj.required_qty = qty
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class Lead(SellingController, CRMNote):
|
|||||||
"last_name": self.last_name,
|
"last_name": self.last_name,
|
||||||
"salutation": self.salutation,
|
"salutation": self.salutation,
|
||||||
"gender": self.gender,
|
"gender": self.gender,
|
||||||
"job_title": self.job_title,
|
"designation": self.job_title,
|
||||||
"company_name": self.company_name,
|
"company_name": self.company_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -432,7 +432,6 @@ scheduler_events = {
|
|||||||
"erpnext.controllers.accounts_controller.update_invoice_status",
|
"erpnext.controllers.accounts_controller.update_invoice_status",
|
||||||
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
|
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
|
||||||
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
|
"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.stock.doctype.serial_no.serial_no.update_maintenance_status",
|
||||||
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
|
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
|
||||||
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
|
"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_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.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.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||||
|
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||||
],
|
],
|
||||||
"monthly_long": [
|
"monthly_long": [
|
||||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class LowerDeductionCertificate(Document):
|
|||||||
"supplier": self.supplier,
|
"supplier": self.supplier,
|
||||||
"tax_withholding_category": self.tax_withholding_category,
|
"tax_withholding_category": self.tax_withholding_category,
|
||||||
"name": ("!=", self.name),
|
"name": ("!=", self.name),
|
||||||
|
"company": self.company,
|
||||||
},
|
},
|
||||||
["name", "valid_from", "valid_upto"],
|
["name", "valid_from", "valid_upto"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ import json
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from babel import Locale
|
|
||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import formatdate, getdate, today
|
from frappe.utils import formatdate, getdate, today
|
||||||
from holidays import country_holidays
|
|
||||||
from holidays.utils import list_supported_countries
|
|
||||||
|
|
||||||
|
|
||||||
class OverlapError(frappe.ValidationError):
|
class OverlapError(frappe.ValidationError):
|
||||||
@@ -40,6 +37,8 @@ class HolidayList(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_supported_countries(self):
|
def get_supported_countries(self):
|
||||||
|
from holidays.utils import list_supported_countries
|
||||||
|
|
||||||
subdivisions_by_country = list_supported_countries()
|
subdivisions_by_country = list_supported_countries()
|
||||||
countries = [
|
countries = [
|
||||||
{"value": country, "label": local_country_name(country)}
|
{"value": country, "label": local_country_name(country)}
|
||||||
@@ -52,6 +51,8 @@ class HolidayList(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_local_holidays(self):
|
def get_local_holidays(self):
|
||||||
|
from holidays import country_holidays
|
||||||
|
|
||||||
if not self.country:
|
if not self.country:
|
||||||
throw(_("Please select a 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:
|
def local_country_name(country_code: str) -> str:
|
||||||
"""Return the localized country name for the given country code."""
|
"""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
|
import frappe
|
||||||
from frappe.utils import getdate
|
from frappe.utils import getdate
|
||||||
|
|
||||||
|
from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
|
||||||
|
|
||||||
|
|
||||||
class TestHolidayList(unittest.TestCase):
|
class TestHolidayList(unittest.TestCase):
|
||||||
def test_holiday_list(self):
|
def test_holiday_list(self):
|
||||||
@@ -58,6 +60,16 @@ class TestHolidayList(unittest.TestCase):
|
|||||||
self.assertIn(date(2023, 4, 10), holidays)
|
self.assertIn(date(2023, 4, 10), holidays)
|
||||||
self.assertNotIn(date(2023, 5, 1), 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(
|
def make_holiday_list(
|
||||||
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
|
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ def get_data():
|
|||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "against_sales_order"],
|
"Sales Order": ["items", "against_sales_order"],
|
||||||
|
"Sales Invoice": ["items", "against_sales_invoice"],
|
||||||
"Material Request": ["items", "material_request"],
|
"Material Request": ["items", "material_request"],
|
||||||
"Purchase Order": ["items", "purchase_order"],
|
"Purchase Order": ["items", "purchase_order"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -228,7 +228,8 @@ class MaterialRequest(BuyingController):
|
|||||||
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||||
|
|
||||||
if mr_qty_allowance:
|
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:
|
if d.ordered_qty and d.ordered_qty > allowed_qty:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ class StockEntry(StockController):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# If line items are more than 100 or record is older than 6 months
|
# 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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -607,7 +607,7 @@ class StockReconciliation(StockController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if sl_entries:
|
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(
|
def get_batch_qty_for_stock_reco(
|
||||||
|
|||||||
@@ -594,6 +594,67 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
|
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
|
||||||
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
|
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):
|
def make_return_subcontracting_receipt(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user