Merge pull request #36546 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-08-09 08:35:06 +05:30
committed by GitHub
39 changed files with 1170 additions and 281 deletions

View File

@@ -238,7 +238,8 @@ class PaymentEntry(AccountsController):
d.payment_term
and (
(flt(d.allocated_amount)) > 0
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
and latest.payment_term_outstanding
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
)
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
):
@@ -635,7 +636,9 @@ class PaymentEntry(AccountsController):
if not self.apply_tax_withholding_amount:
return
net_total = self.paid_amount
order_amount = self.get_order_net_total()
net_total = flt(order_amount) + flt(self.unallocated_amount)
# Adding args as purchase invoice to get TDS amount
args = frappe._dict(
@@ -680,6 +683,20 @@ class PaymentEntry(AccountsController):
for d in to_remove:
self.remove(d)
def get_order_net_total(self):
if self.party_type == "Supplier":
doctype = "Purchase Order"
else:
doctype = "Sales Order"
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
tax_withholding_net_total = frappe.db.get_value(
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
)
return tax_withholding_net_total
def apply_taxes(self):
self.initialize_taxes()
self.determine_exclusive_rate()

View File

@@ -976,30 +976,6 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount")
)
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
precision_loss = self.get("base_net_total") - flt(
self.get("net_total") * self.conversion_rate, self.precision("net_total")
)
if precision_loss:
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": self.supplier,
"credit": precision_loss,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
)
def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")

View File

@@ -1670,6 +1670,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
automatically_fetch_payment_terms()
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
0,
)
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template"
po.save()
po.submit()
pr = create_pr_against_po(po.name, received_qty=4)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
1,
)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
0,
)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -1075,6 +1075,7 @@ class SalesInvoice(SellingController):
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
self.make_discount_gl_entries(gl_entries)
# merge gl entries before adding pos entries

View File

@@ -15,6 +15,7 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Delivery Note": ["items", "delivery_note"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [

View File

@@ -2049,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
expected_values = dict(
(d[0], d)
for d in [
[si.debit_to, 1500, 0.0],
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
["Sales - _TC", 0.0, 1271.18],
]
)
expected_values = [
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
[si.debit_to, 1500, 0.0],
["Round Off - _TC", 0.01, 0.01],
["Sales - _TC", 0.0, 1271.18],
]
gl_entries = frappe.db.sql(
"""select account, debit, credit
"""select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
group by account
order by account asc""",
si.name,
as_dict=1,
)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
@@ -2125,13 +2124,14 @@ class TestSalesInvoice(unittest.TestCase):
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.01, 0],
["Round Off - _TC", 0.02, 0.01],
]
)
gl_entries = frappe.db.sql(
"""select account, debit, credit
"""select account, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
group by account
order by account asc""",
si.name,
as_dict=1,
@@ -3316,6 +3316,13 @@ class TestSalesInvoice(unittest.TestCase):
)
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_return_negative_rate(self):
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
self.assertRaises(frappe.ValidationError, si.save)
si.items[0].rate = 10
si.save()
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()

View File

@@ -476,7 +476,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
if (threshold and inv.tax_withholding_net_total >= threshold) or (
if inv.doctype != "Payment Entry":
tax_withholding_net_total = inv.base_tax_withholding_net_total
else:
tax_withholding_net_total = inv.tax_withholding_net_total
if (threshold and tax_withholding_net_total >= threshold) or (
cumulative_threshold and supp_credit_amt >= cumulative_threshold
):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(

View File

@@ -321,6 +321,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in reversed(orders):
d.cancel()
def test_tds_deduction_for_po_via_payment_entry(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.db.set_value(
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
)
order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
# Add some tax on the order
order.append(
"taxes",
{
"category": "Total",
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"tax_amount": 8000,
"description": "Test",
"add_deduct_tax": "Add",
},
)
order.save()
order.apply_tds = 1
order.tax_withholding_category = "Cumulative Threshold TDS"
order.submit()
self.assertEqual(order.taxes[0].tax_amount, 4000)
payment = get_payment_entry(order.doctype, order.name)
payment.apply_tax_withholding_amount = 1
payment.tax_withholding_category = "Cumulative Threshold TDS"
payment.submit()
self.assertEqual(payment.taxes[0].tax_amount, 4000)
def test_multi_category_single_supplier(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
@@ -578,6 +614,7 @@ def create_records():
"Test TDS Supplier5",
"Test TDS Supplier6",
"Test TDS Supplier7",
"Test TDS Supplier8",
]:
if frappe.db.exists("Supplier", name):
continue

View File

@@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import (
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
def get_partywise_advanced_payment_amount(
party_type, posting_date=None, future_payment=0, company=None, party=None
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
):
cond = "1=1"
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select(gle.party)
.where(
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
)
.groupby(gle.party)
)
if account_type == "Receivable":
query = query.select(Sum(gle.credit).as_("amount"))
else:
query = query.select(Sum(gle.debit).as_("amount"))
if posting_date:
if future_payment:
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
else:
cond = "posting_date <= '{0}'".format(posting_date)
query = query.where(gle.posting_date <= posting_date)
if company:
cond += "and company = {0}".format(frappe.db.escape(company))
query = query.where(gle.company == company)
if party:
cond += "and party = {0}".format(frappe.db.escape(party))
query = query.where(gle.party == party)
data = frappe.db.sql(
""" SELECT party, sum({0}) as amount
FROM `tabGL Entry`
WHERE
party_type = %s and against_voucher is null
and is_cancelled = 0
and {1} GROUP BY party""".format(
("credit") if party_type == "Customer" else "debit", cond
),
party_type,
)
data = query.run(as_dict=True)
if data:
return frappe._dict(data)

View File

@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
def execute(filters=None):
args = {
"party_type": "Supplier",
"account_type": "Payable",
"naming_by": ["Buying Settings", "supp_master_name"],
}
return ReceivablePayableReport(filters).run(args)

View File

@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
def execute(filters=None):
args = {
"party_type": "Supplier",
"account_type": "Payable",
"naming_by": ["Buying Settings", "supp_master_name"],
}
return AccountsReceivableSummary(filters).run(args)

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date
from frappe.query_builder.functions import Date, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
args = {
"party_type": "Customer",
"account_type": "Receivable",
"naming_by": ["Selling Settings", "cust_master_name"],
}
return ReceivablePayableReport(filters).run(args)
@@ -70,8 +70,11 @@ class ReceivablePayableReport(object):
"Company", self.filters.get("company"), "default_currency"
)
self.currency_precision = get_currency_precision() or 2
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
self.party_type = self.filters.party_type
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
self.account_type = self.filters.account_type
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
@@ -197,6 +200,7 @@ class ReceivablePayableReport(object):
# no invoice, this is an invoice / stand-alone payment / credit note
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
row.party_type = ple.party_type
return row
def update_voucher_balance(self, ple):
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
return
# amount in "Party Currency", if its supplied. If not, amount in company currency
if self.filters.get(scrub(self.party_type)):
amount = ple.amount_in_account_currency
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
amount = ple.amount_in_account_currency
else:
amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
def get_invoice_details(self):
self.invoice_details = frappe._dict()
if self.party_type == "Customer":
if self.account_type == "Receivable":
si_list = frappe.db.sql(
"""
select name, due_date, po_no
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
d.sales_person
)
if self.party_type == "Supplier":
if self.account_type == "Payable":
for pi in frappe.db.sql(
"""
select name, due_date, bill_no, bill_date
@@ -421,8 +426,10 @@ class ReceivablePayableReport(object):
# customer / supplier name
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
if self.filters.get(scrub(self.filters.party_type)):
row.currency = row.account_currency
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
row.currency = row.account_currency
break
else:
row.currency = self.company_currency
@@ -532,65 +539,67 @@ class ReceivablePayableReport(object):
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
def get_future_payments_from_payment_entry(self):
return frappe.db.sql(
"""
select
ref.reference_name as invoice_no,
payment_entry.party,
payment_entry.party_type,
payment_entry.posting_date as future_date,
ref.allocated_amount as future_amount,
payment_entry.reference_no as future_ref
from
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
on
(ref.parent = payment_entry.name)
where
payment_entry.docstatus < 2
and payment_entry.posting_date > %s
and payment_entry.party_type = %s
""",
(self.filters.report_date, self.party_type),
as_dict=1,
)
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
return (
frappe.qb.from_(pe)
.inner_join(pe_ref)
.on(pe_ref.parent == pe.name)
.select(
(pe_ref.reference_name).as_("invoice_no"),
pe.party,
pe.party_type,
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
)
.where(
(pe.docstatus < 2)
& (pe.posting_date > self.filters.report_date)
& (pe.party_type.isin(self.party_type))
)
).run(as_dict=True)
def get_future_payments_from_journal_entry(self):
if self.filters.get("party"):
amount_field = (
"jea.debit_in_account_currency - jea.credit_in_account_currency"
if self.party_type == "Supplier"
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
)
else:
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
return frappe.db.sql(
"""
select
jea.reference_name as invoice_no,
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
query = (
frappe.qb.from_(je)
.inner_join(jea)
.on(jea.parent == je.name)
.select(
jea.reference_name.as_("invoice_no"),
jea.party,
jea.party_type,
je.posting_date as future_date,
sum('{0}') as future_amount,
je.cheque_no as future_ref
from
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
on
(jea.parent = je.name)
where
je.docstatus < 2
and je.posting_date > %s
and jea.party_type = %s
and jea.reference_name is not null and jea.reference_name != ''
group by je.name, jea.reference_name
having future_amount > 0
""".format(
amount_field
),
(self.filters.report_date, self.party_type),
as_dict=1,
je.posting_date.as_("future_date"),
je.cheque_no.as_("future_ref"),
)
.where(
(je.docstatus < 2)
& (je.posting_date > self.filters.report_date)
& (jea.party_type.isin(self.party_type))
& (jea.reference_name.isnotnull())
& (jea.reference_name != "")
)
)
if self.filters.get("party"):
if self.account_type == "Payable":
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
else:
query = query.select(
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
)
query = query.having(qb.Field("future_amount") > 0)
return query.run(as_dict=True)
def allocate_future_payments(self, row):
# future payments are captured in additional columns
# this method allocates pending future payments against a voucher to
@@ -619,13 +628,17 @@ class ReceivablePayableReport(object):
row.future_ref = ", ".join(row.future_ref)
def get_return_entries(self):
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
party_field = scrub(self.filters.party_type)
if self.filters.get(party_field):
filters.update({party_field: self.filters.get(party_field)})
or_filters = {}
for party_type in self.party_type:
party_field = scrub(party_type)
if self.filters.get(party_field):
or_filters.update({party_field: self.filters.get(party_field)})
self.return_entries = frappe._dict(
frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
frappe.get_all(
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
)
)
def set_ageing(self, row):
@@ -716,6 +729,7 @@ class ReceivablePayableReport(object):
)
.where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter))
.where(Criterion.any(self.or_filters))
)
if self.filters.get("group_by_party"):
@@ -746,16 +760,18 @@ class ReceivablePayableReport(object):
def prepare_conditions(self):
self.qb_selection_filter = []
party_type_field = scrub(self.party_type)
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
self.or_filters = []
for party_type in self.party_type:
party_type_field = scrub(party_type)
self.or_filters.append(self.ple.party_type == party_type)
self.add_common_filters(party_type_field=party_type_field)
self.add_common_filters(party_type_field=party_type_field)
if party_type_field == "customer":
self.add_customer_filters()
if party_type_field == "customer":
self.add_customer_filters()
elif party_type_field == "supplier":
self.add_supplier_filters()
elif party_type_field == "supplier":
self.add_supplier_filters()
if self.filters.cost_center:
self.get_cost_center_conditions()
@@ -784,11 +800,10 @@ class ReceivablePayableReport(object):
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
else:
# get GL with "receivable" or "payable" account_type
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
accounts = [
d.name
for d in frappe.get_all(
"Account", filters={"account_type": account_type, "company": self.filters.company}
"Account", filters={"account_type": self.account_type, "company": self.filters.company}
)
]
@@ -878,7 +893,7 @@ class ReceivablePayableReport(object):
def get_party_details(self, party):
if not party in self.party_details:
if self.party_type == "Customer":
if self.account_type == "Receivable":
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
if self.filters.get("sales_partner"):
@@ -901,14 +916,20 @@ class ReceivablePayableReport(object):
self.columns = []
self.add_column("Posting Date", fieldtype="Date")
self.add_column(
label=_(self.party_type),
label="Party Type",
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label="Party",
fieldname="party",
fieldtype="Link",
options=self.party_type,
fieldtype="Dynamic Link",
options="party_type",
width=180,
)
self.add_column(
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
label=self.account_type + " Account",
fieldname="party_account",
fieldtype="Link",
options="Account",
@@ -916,13 +937,19 @@ class ReceivablePayableReport(object):
)
if self.party_naming_by == "Naming Series":
if self.account_type == "Payable":
label = "Supplier Name"
fieldname = "supplier_name"
else:
label = "Customer Name"
fieldname = "customer_name"
self.add_column(
_("{0} Name").format(self.party_type),
fieldname=scrub(self.party_type) + "_name",
label=label,
fieldname=fieldname,
fieldtype="Data",
)
if self.party_type == "Customer":
if self.account_type == "Receivable":
self.add_column(
_("Customer Contact"),
fieldname="customer_primary_contact",
@@ -942,7 +969,7 @@ class ReceivablePayableReport(object):
self.add_column(label="Due Date", fieldtype="Date")
if self.party_type == "Supplier":
if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
@@ -952,7 +979,7 @@ class ReceivablePayableReport(object):
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
self.add_column(_("Paid Amount"), fieldname="paid")
if self.party_type == "Customer":
if self.account_type == "Receivable":
self.add_column(_("Credit Note"), fieldname="credit_note")
else:
# note: fieldname is still `credit_note`
@@ -970,7 +997,7 @@ class ReceivablePayableReport(object):
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
if self.filters.party_type == "Customer":
if self.filters.account_type == "Receivable":
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
# comma separated list of linked delivery notes
@@ -991,7 +1018,7 @@ class ReceivablePayableReport(object):
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
if self.filters.party_type == "Supplier":
if self.filters.account_type == "Payable":
self.add_column(
label=_("Supplier Group"),
fieldname="supplier_group",

View File

@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
def execute(filters=None):
args = {
"party_type": "Customer",
"account_type": "Receivable",
"naming_by": ["Selling Settings", "cust_master_name"],
}
@@ -21,7 +21,10 @@ def execute(filters=None):
class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args):
self.party_type = args.get("party_type")
self.account_type = args.get("account_type")
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
)
@@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.get_party_total(args)
party = None
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
party = self.filters.get(scrub(party_type))
party_advance_amount = (
get_partywise_advanced_payment_amount(
self.party_type,
self.filters.report_date,
self.filters.show_future_payments,
self.filters.company,
party=self.filters.get(scrub(self.party_type)),
party=party,
account_type=self.account_type,
)
or {}
)
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
row.party = party
if self.party_naming_by == "Naming Series":
row.party_name = frappe.get_cached_value(
self.party_type, party, scrub(self.party_type) + "_name"
)
if self.account_type == "Payable":
doctype = "Supplier"
fieldname = "supplier_name"
else:
doctype = "Customer"
fieldname = "customer_name"
row.party_name = frappe.get_cached_value(doctype, party, fieldname)
row.update(party_dict)
@@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# set territory, customer_group, sales person etc
self.set_party_details(d)
self.party_total[d.party].update({"party_type": d.party_type})
def init_party_total(self, row):
self.party_total.setdefault(
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
def get_columns(self):
self.columns = []
self.add_column(
label=_(self.party_type),
label=_("Party Type"),
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label=_("Party"),
fieldname="party",
fieldtype="Link",
options=self.party_type,
fieldtype="Dynamic Link",
options="party_type",
width=180,
)
if self.party_naming_by == "Naming Series":
self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
self.add_column(
label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
fieldname="party_name",
fieldtype="Data",
)
credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
self.add_column(_("Advance Amount"), fieldname="advance")
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
@@ -159,7 +183,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
if self.party_type == "Customer":
if self.account_type == "Receivable":
self.add_column(
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
)

View 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)

View File

@@ -659,11 +659,12 @@ def set_gl_entries_by_account(
& (gle.posting_date <= to_date)
& (account.lft >= root_lft)
& (account.rgt <= root_rgt)
& (account.root_type <= root_type)
)
.orderby(gle.account, gle.posting_date)
)
if root_type:
query = query.where(account.root_type == root_type)
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
if additional_conditions:
query = query.where(Criterion.all(additional_conditions))

View File

@@ -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()
};

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -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)

View File

@@ -148,17 +148,33 @@ class Asset(AccountsController):
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
def validate_cost_center(self):
if not self.cost_center:
return
cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
if cost_center_company != self.company:
frappe.throw(
_("Selected Cost Center {} doesn't belongs to {}").format(
frappe.bold(self.cost_center), frappe.bold(self.company)
),
title=_("Invalid Cost Center"),
if self.cost_center:
cost_center_company, cost_center_is_group = frappe.db.get_value(
"Cost Center", self.cost_center, ["company", "is_group"]
)
if cost_center_company != self.company:
frappe.throw(
_("Cost Center {} doesn't belong to Company {}").format(
frappe.bold(self.cost_center), frappe.bold(self.company)
),
title=_("Invalid Cost Center"),
)
if cost_center_is_group:
frappe.throw(
_(
"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
).format(frappe.bold(self.cost_center)),
title=_("Invalid Cost Center"),
)
else:
if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
frappe.throw(
_(
"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
).format(frappe.bold(self.company)),
title=_("Missing Cost Center"),
)
def validate_in_use_date(self):
if not self.available_for_use_date:
@@ -946,7 +962,9 @@ class Asset(AccountsController):
@frappe.whitelist()
def get_manual_depreciation_entries(self):
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
(_, _, depreciation_expense_account) = get_depreciation_accounts(
self.asset_category, self.company
)
gle = frappe.qb.DocType("GL Entry")
@@ -1185,10 +1203,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
def make_journal_entry(asset_name):
asset = frappe.get_doc("Asset", asset_name)
(
fixed_asset_account,
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset)
) = get_depreciation_accounts(asset.asset_category, asset.company)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]

View File

@@ -4,6 +4,8 @@
import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Max, Min
from frappe.utils import (
add_months,
cint,
@@ -36,9 +38,40 @@ def post_depreciation_entries(date=None):
failed_asset_names = []
error_log_names = []
for asset_name in get_depreciable_assets(date):
depreciable_assets = get_depreciable_assets(date)
credit_and_debit_accounts_for_asset_category_and_company = {}
depreciation_cost_center_and_depreciation_series_for_company = (
get_depreciation_cost_center_and_depreciation_series_for_company()
)
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for asset in depreciable_assets:
asset_name, asset_category, asset_company, sch_start_idx, sch_end_idx = asset
if (
asset_category,
asset_company,
) not in credit_and_debit_accounts_for_asset_category_and_company:
credit_and_debit_accounts_for_asset_category_and_company.update(
{
(asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company(
asset_category, asset_company
),
}
)
try:
make_depreciation_entry(asset_name, date)
make_depreciation_entry(
asset_name,
date,
sch_start_idx,
sch_end_idx,
credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
depreciation_cost_center_and_depreciation_series_for_company[asset_company],
accounting_dimensions,
)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
@@ -54,115 +87,226 @@ def post_depreciation_entries(date=None):
def get_depreciable_assets(date):
return frappe.db.sql_list(
"""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
and a.status in ('Submitted', 'Partially Depreciated')
and ifnull(ds.journal_entry, '')=''""",
date,
a = frappe.qb.DocType("Asset")
ds = frappe.qb.DocType("Depreciation Schedule")
res = (
frappe.qb.from_(a)
.join(ds)
.on(a.name == ds.parent)
.select(a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
.where(a.calculate_depreciation == 1)
.where(a.docstatus == 1)
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
.where(ds.journal_entry.isnull())
.where(ds.schedule_date <= date)
.groupby(a.name)
.orderby(a.creation, order=Order.desc)
)
acc_frozen_upto = get_acc_frozen_upto()
if acc_frozen_upto:
res = res.where(ds.schedule_date > acc_frozen_upto)
res = res.run()
return res
def get_acc_frozen_upto():
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
if not acc_frozen_upto:
return
frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "frozen_accounts_modifier"
)
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
return getdate(acc_frozen_upto)
return
def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
(
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset_category, company)
credit_account, debit_account = get_credit_and_debit_accounts(
accumulated_depreciation_account, depreciation_expense_account
)
return (credit_account, debit_account)
def get_depreciation_cost_center_and_depreciation_series_for_company():
company_names = frappe.db.get_all("Company", pluck="name")
res = {}
for company_name in company_names:
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
res.update({company_name: (depreciation_cost_center, depreciation_series)})
return res
@frappe.whitelist()
def make_depreciation_entry(asset_name, date=None):
def make_depreciation_entry(
asset_name,
date=None,
sch_start_idx=None,
sch_end_idx=None,
credit_and_debit_accounts=None,
depreciation_cost_center_and_depreciation_series=None,
accounting_dimensions=None,
):
frappe.has_permission("Journal Entry", throw=True)
if not date:
date = today()
asset = frappe.get_doc("Asset", asset_name)
(
fixed_asset_account,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
if credit_and_debit_accounts:
credit_account, debit_account = credit_and_debit_accounts
else:
credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
asset.asset_category, asset.company
)
if depreciation_cost_center_and_depreciation_series:
depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
else:
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
if not accounting_dimensions:
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for d in asset.get("schedules"):
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.posting_date = d.schedule_date
je.company = asset.company
je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
depreciation_posting_error = None
credit_account, debit_account = get_credit_and_debit_accounts(
accumulated_depreciation_account, depreciation_expense_account
for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]:
try:
_make_journal_entry_for_depreciation(
asset,
date,
d,
sch_start_idx,
sch_end_idx,
depreciation_cost_center,
depreciation_series,
credit_account,
debit_account,
accounting_dimensions,
)
credit_entry = {
"account": credit_account,
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
debit_entry = {
"account": debit_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
for dimension in accounting_dimensions:
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
credit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
debit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
d.db_set("journal_entry", je.name)
if not je.meta.get_workflow():
je.submit()
idx = cint(d.finance_book_id)
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation -= d.depreciation_amount
finance_books.db_update()
asset.db_set("depr_entry_posting_status", "Successful")
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
depreciation_posting_error = e
asset.set_status()
return asset
if not depreciation_posting_error:
asset.db_set("depr_entry_posting_status", "Successful")
return asset
raise depreciation_posting_error
def get_depreciation_accounts(asset):
def _make_journal_entry_for_depreciation(
asset,
date,
depr_schedule,
sch_start_idx,
sch_end_idx,
depreciation_cost_center,
depreciation_series,
credit_account,
debit_account,
accounting_dimensions,
):
if not (sch_start_idx and sch_end_idx) and not (
not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date)
):
return
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.posting_date = depr_schedule.schedule_date
je.company = asset.company
je.finance_book = depr_schedule.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(
asset.name, depr_schedule.depreciation_amount
)
credit_entry = {
"account": credit_account,
"credit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
debit_entry = {
"account": debit_account,
"debit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
}
for dimension in accounting_dimensions:
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
credit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
debit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
depr_schedule.db_set("journal_entry", je.name)
if not je.meta.get_workflow():
je.submit()
idx = cint(depr_schedule.finance_book_id)
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation -= depr_schedule.depreciation_amount
finance_books.db_update()
def get_depreciation_accounts(asset_category, company):
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
accounts = frappe.db.get_value(
"Asset Category Account",
filters={"parent": asset.asset_category, "company_name": asset.company},
filters={"parent": asset_category, "company_name": company},
fieldname=[
"fixed_asset_account",
"accumulated_depreciation_account",
@@ -178,7 +322,7 @@ def get_depreciation_accounts(asset):
if not accumulated_depreciation_account or not depreciation_expense_account:
accounts = frappe.get_cached_value(
"Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
)
if not accumulated_depreciation_account:
@@ -193,7 +337,7 @@ def get_depreciation_accounts(asset):
):
frappe.throw(
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
asset.asset_category, asset.company
asset_category, company
)
)
@@ -533,8 +677,8 @@ def get_gl_entries_on_asset_disposal(
def get_asset_details(asset, finance_book=None):
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
asset
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
asset.asset_category, asset.company
)
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center

View File

@@ -50,10 +50,10 @@ class AssetValueAdjustment(Document):
def make_depreciation_entry(self):
asset = frappe.get_doc("Asset", self.asset)
(
fixed_asset_account,
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset)
) = get_depreciation_accounts(asset.asset_category, asset.company)
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]

View File

@@ -369,7 +369,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "qty"]
child_columns: ["item_code", "qty", "ordered_qty"]
})
}, __("Get Items From"));

View File

@@ -25,6 +25,7 @@
"col_break_email_1",
"email_template",
"preview",
"send_attached_files",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -285,13 +286,20 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"default": "1",
"description": "If enabled, all files attached to this document will be attached to each email",
"fieldname": "send_attached_files",
"fieldtype": "Check",
"label": "Send Attached Files"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-01-31 23:22:06.684694",
"modified": "2023-07-27 16:41:48.468873",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -209,7 +209,9 @@ class RequestforQuotation(BuyingController):
if preview:
return message
attachments = self.get_attachments()
attachments = None
if self.send_attached_files:
attachments = self.get_attachments()
self.send_email(data, sender, subject, message, attachments)

View File

@@ -31,6 +31,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
apply_pricing_rule_on_transaction,
get_applied_pricing_rules,
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import (
get_party_account,
get_party_account_currency,
@@ -1023,6 +1024,33 @@ class AccountsController(TransactionBase):
)
)
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
)
precision_loss = self.get("base_net_total") - flt(
self.get("net_total") * self.conversion_rate, self.precision("net_total")
)
credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
if precision_loss:
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": against,
credit_or_debit: precision_loss,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
)
def update_against_document_in_jv(self):
"""
Links invoice and advance voucher:
@@ -1672,8 +1700,13 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
allocate_payment_based_on_payment_terms = frappe.db.get_value(
"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
)
if not (
automatically_fetch_payment_terms
and allocate_payment_based_on_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
for d in self.get("payment_schedule"):

View File

@@ -233,6 +233,9 @@ class StatusUpdater(Document):
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
args["name"] = d.get(args["join_field"])

View File

@@ -460,7 +460,7 @@ class SubcontractingController(StockController):
"allow_zero_valuation": 1,
}
)
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
rm_obj.rate = get_incoming_rate(args)
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = qty

View File

@@ -185,7 +185,7 @@ class Lead(SellingController, CRMNote):
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
"job_title": self.job_title,
"designation": self.job_title,
"company_name": self.company_name,
}
)

View File

@@ -432,7 +432,6 @@ scheduler_events = {
"erpnext.controllers.accounts_controller.update_invoice_status",
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
@@ -459,6 +458,7 @@ scheduler_events = {
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",

View File

@@ -34,6 +34,7 @@ class LowerDeductionCertificate(Document):
"supplier": self.supplier,
"tax_withholding_category": self.tax_withholding_category,
"name": ("!=", self.name),
"company": self.company,
},
["name", "valid_from", "valid_upto"],
as_dict=True,

View File

@@ -6,12 +6,9 @@ import json
from datetime import date
import frappe
from babel import Locale
from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import formatdate, getdate, today
from holidays import country_holidays
from holidays.utils import list_supported_countries
class OverlapError(frappe.ValidationError):
@@ -40,6 +37,8 @@ class HolidayList(Document):
@frappe.whitelist()
def get_supported_countries(self):
from holidays.utils import list_supported_countries
subdivisions_by_country = list_supported_countries()
countries = [
{"value": country, "label": local_country_name(country)}
@@ -52,6 +51,8 @@ class HolidayList(Document):
@frappe.whitelist()
def get_local_holidays(self):
from holidays import country_holidays
if not self.country:
throw(_("Please select a country"))
@@ -169,4 +170,6 @@ def is_holiday(holiday_list, date=None):
def local_country_name(country_code: str) -> str:
"""Return the localized country name for the given country code."""
return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)
from babel import Locale
return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code)

View File

@@ -8,6 +8,8 @@ from datetime import date, timedelta
import frappe
from frappe.utils import getdate
from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
class TestHolidayList(unittest.TestCase):
def test_holiday_list(self):
@@ -58,6 +60,16 @@ class TestHolidayList(unittest.TestCase):
self.assertIn(date(2023, 4, 10), holidays)
self.assertNotIn(date(2023, 5, 1), holidays)
def test_localized_country_names(self):
lang = frappe.local.lang
frappe.local.lang = "en-gb"
self.assertEqual(local_country_name("IN"), "India")
self.assertEqual(local_country_name("DE"), "Germany")
frappe.local.lang = "de"
self.assertEqual(local_country_name("DE"), "Deutschland")
frappe.local.lang = lang
def make_holiday_list(
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None

View File

@@ -11,6 +11,7 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "against_sales_order"],
"Sales Invoice": ["items", "against_sales_invoice"],
"Material Request": ["items", "material_request"],
"Purchase Order": ["items", "purchase_order"],
},

View File

@@ -228,7 +228,8 @@ class MaterialRequest(BuyingController):
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty"))
if d.ordered_qty and d.ordered_qty > allowed_qty:
frappe.throw(
_(

View File

@@ -194,7 +194,7 @@ class StockEntry(StockController):
return False
# If line items are more than 100 or record is older than 6 months
if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6:
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
return True
return False

View File

@@ -607,7 +607,7 @@ class StockReconciliation(StockController):
)
if sl_entries:
self.make_sl_entries(sl_entries)
self.make_sl_entries(sl_entries, allow_negative_stock=True)
def get_batch_qty_for_stock_reco(

View File

@@ -594,6 +594,67 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
def test_subcontracting_receipt_raw_material_rate(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
# Step - 1: Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
# Step - 2: Create FG and RM Items
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
rm_item1 = make_item(properties={"is_stock_item": 1}).name
rm_item2 = make_item(properties={"is_stock_item": 1}).name
# Step - 3: Create BOM for FG Item
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
for rm_item in bom.items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
bom = bom.name
# Step - 4: Create PO and SCO
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 100,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 100,
},
]
sco = get_subcontracting_order(service_items=service_items)
for rm_item in sco.supplied_items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
# Step - 5: Inward Raw Materials
rm_items = get_rm_items(sco.supplied_items)
for rm_item in rm_items:
rm_item["rate"] = 100
itemwise_details = make_stock_in_entry(rm_items=rm_items)
# Step - 6: Transfer RM's to Subcontractor
se = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
for item in se.items:
self.assertEqual(item.qty, 100)
self.assertEqual(item.basic_rate, 100)
self.assertEqual(item.amount, item.qty * item.basic_rate)
# Step - 7: Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
scr.load_from_db()
for rm_item in scr.supplied_items:
self.assertEqual(rm_item.consumed_qty, 100)
self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)