Merge pull request #43254 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-09-18 13:01:08 +05:30
committed by GitHub
30 changed files with 794 additions and 136 deletions

View File

@@ -22,8 +22,10 @@ class TestCostCenterAllocation(unittest.TestCase):
cost_centers = [
"Main Cost Center 1",
"Main Cost Center 2",
"Main Cost Center 3",
"Sub Cost Center 1",
"Sub Cost Center 2",
"Sub Cost Center 3",
]
for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company")
@@ -36,7 +38,7 @@ class TestCostCenterAllocation(unittest.TestCase):
)
jv = make_journal_entry(
"_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
"Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
)
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
@@ -120,7 +122,7 @@ class TestCostCenterAllocation(unittest.TestCase):
def test_valid_from_based_on_existing_gle(self):
# GLE posted against Sub Cost Center 1 on today
jv = make_journal_entry(
"_Test Cash - _TC",
"Cash - _TC",
"Sales - _TC",
100,
cost_center="Main Cost Center 1 - _TC",
@@ -141,6 +143,53 @@ class TestCostCenterAllocation(unittest.TestCase):
jv.cancel()
def test_multiple_cost_center_allocation_on_same_main_cost_center(self):
coa1 = create_cost_center_allocation(
"_Test Company",
"Main Cost Center 3 - _TC",
{"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40},
valid_from=add_days(today(), -5),
)
coa2 = create_cost_center_allocation(
"_Test Company",
"Main Cost Center 3 - _TC",
{"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50},
valid_from=add_days(today(), -1),
)
jv = make_journal_entry(
"Cash - _TC",
"Sales - _TC",
100,
cost_center="Main Cost Center 3 - _TC",
posting_date=today(),
submit=True,
)
expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.cost_center, gle.debit, gle.credit)
.where(gle.voucher_type == "Journal Entry")
.where(gle.voucher_no == jv.name)
.where(gle.account == "Sales - _TC")
.orderby(gle.cost_center)
).run(as_dict=1)
self.assertTrue(gl_entries)
for gle in gl_entries:
self.assertTrue(gle.cost_center in expected_values)
self.assertEqual(gle.debit, 0)
self.assertEqual(gle.credit, expected_values[gle.cost_center])
coa1.cancel()
coa2.cancel()
jv.cancel()
def create_cost_center_allocation(
company,

View File

@@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
accounts_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
row.exchange_rate = 1;
$.each(doc.accounts, function (i, d) {
if (d.account && d.party && d.party_type) {
row.account = d.account;
row.party = d.party;
row.party_type = d.party_type;
row.exchange_rate = d.exchange_rate;
}
});
// set difference
if (doc.difference) {
if (doc.difference > 0) {
row.credit_in_account_currency = doc.difference;
row.credit_in_account_currency = doc.difference / row.exchange_rate;
row.credit = doc.difference;
} else {
row.debit_in_account_currency = -doc.difference;
row.debit_in_account_currency = -doc.difference / row.exchange_rate;
row.debit = -doc.difference;
}
}
@@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, {
callback: function (r) {
if (r.message) {
$.extend(d, r.message);
erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn);
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn);
refresh_field("accounts");
}
@@ -687,4 +690,26 @@ $.extend(erpnext.journal_entry, {
});
}
},
set_amount_on_last_row: function (frm, dt, dn) {
let row = locals[dt][dn];
let length = frm.doc.accounts.length;
if (row.idx != length) return;
let difference = frm.doc.accounts.reduce((total, row) => {
if (row.idx == length) return total;
return total + row.debit - row.credit;
}, 0);
if (difference) {
if (difference > 0) {
row.credit_in_account_currency = difference / row.exchange_rate;
row.credit = difference;
} else {
row.debit_in_account_currency = -difference / row.exchange_rate;
row.debit = -difference;
}
}
refresh_field("accounts");
},
});

View File

@@ -195,6 +195,11 @@ class JournalEntry(AccountsController):
self.update_booked_depreciation()
def on_update_after_submit(self):
# Flag will be set on Reconciliation
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
if self.flags.get("ignore_reposting_on_reconciliation"):
return
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
if self.needs_repost:
self.validate_for_repost()

View File

@@ -385,7 +385,15 @@ frappe.ui.form.on("Payment Entry", {
payment_type: function (frm) {
if (frm.doc.payment_type == "Internal Transfer") {
$.each(
["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"],
[
"party",
"party_type",
"party_balance",
"paid_from",
"paid_to",
"references",
"total_allocated_amount",
],
function (i, field) {
frm.set_value(field, null);
}

View File

@@ -1791,6 +1791,79 @@ class TestPaymentEntry(FrappeTestCase):
# 'Is Opening' should always be 'No' for normal advance payments
self.assertEqual(gl_with_opening_set, [])
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
def test_delete_linked_exchange_gain_loss_journal(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
debtors = create_account(
account_name="Debtors USD",
parent_account="Accounts Receivable - _TC",
company="_Test Company",
account_currency="USD",
account_type="Receivable",
)
# create a customer
customer = make_customer(customer="_Test Party USD")
cust_doc = frappe.get_doc("Customer", customer)
cust_doc.default_currency = "USD"
test_account_details = {
"company": "_Test Company",
"account": debtors,
}
cust_doc.append("accounts", test_account_details)
cust_doc.save()
# create a sales invoice
si = create_sales_invoice(
customer=customer,
currency="USD",
conversion_rate=83.970000000,
debit_to=debtors,
do_not_save=1,
)
si.party_account_currency = "USD"
si.save()
si.submit()
# create a payment entry for the invoice
pe = get_payment_entry("Sales Invoice", si.name)
pe.reference_no = "1"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 100
pe.source_exchange_rate = 90
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 2710,
},
)
pe.save()
pe.submit()
# check creation of journal entry
jv = frappe.get_all(
"Journal Entry Account",
{"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1},
pluck="parent",
)
self.assertTrue(jv)
# check cancellation of payment entry and journal entry
pe.cancel()
self.assertTrue(pe.docstatus == 2)
self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2)
# check deletion of payment entry and journal entry
pe.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
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
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.stock.doctype.item.test_item import create_item
@@ -1845,6 +1846,78 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
def test_reconciliation_on_closed_period_payment(self):
# create backdated fiscal year
first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)")
prev_fy_start_date = add_years(first_fy_start_date, -1)
prev_fy_end_date = add_days(first_fy_start_date, -1)
create_fiscal_year(
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
)
# make journal entry for previous year
je_1 = frappe.new_doc("Journal Entry")
je_1.posting_date = add_days(prev_fy_start_date, 20)
je_1.company = self.company
je_1.user_remark = "test"
je_1.set(
"accounts",
[
{
"account": self.debit_to,
"cost_center": self.cost_center,
"party_type": "Customer",
"party": self.customer,
"debit_in_account_currency": 0,
"credit_in_account_currency": 1000,
},
{
"account": self.bank,
"cost_center": self.sub_cc.name,
"credit_in_account_currency": 0,
"debit_in_account_currency": 500,
},
{
"account": self.cash,
"cost_center": self.sub_cc.name,
"credit_in_account_currency": 0,
"debit_in_account_currency": 500,
},
],
)
je_1.submit()
# make period closing voucher
pcv = make_period_closing_voucher(
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
)
pcv.reload()
# check if period closing voucher is completed
self.assertEqual(pcv.gle_processing_status, "Completed")
# make journal entry for active year
je_2 = self.create_journal_entry(
acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today()
)
je_2.accounts[0].party_type = "Customer"
je_2.accounts[0].party = self.customer
je_2.submit()
# process reconciliation on closed period payment
pr = self.create_payment_reconciliation(party_is_customer=True)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
je_1.reload()
je_2.reload()
# check whether the payment reconciliation is done on the closed period
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
@@ -1872,3 +1945,61 @@ def make_supplier(supplier_name, currency=None):
return supplier.name
else:
return supplier_name
def create_fiscal_year(company, year_start_date, year_end_date):
fy_docname = frappe.db.exists(
"Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date}
)
if not fy_docname:
fy_doc = frappe.get_doc(
{
"doctype": "Fiscal Year",
"year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}",
"year_start_date": year_start_date,
"year_end_date": year_end_date,
"companies": [{"company": company}],
}
).save()
return fy_doc
else:
fy_doc = frappe.get_doc("Fiscal Year", fy_docname)
if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}):
fy_doc.append("companies", {"company": company})
fy_doc.save()
return fy_doc
def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True):
from erpnext.accounts.doctype.account.test_account import create_account
parent_account = frappe.db.get_value(
"Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name"
)
surplus_account = create_account(
account_name="Reserve and Surplus",
is_group=0,
company=company,
root_type="Liability",
report_type="Balance Sheet",
account_currency="INR",
parent_account=parent_account,
doctype="Account",
)
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": posting_date or today(),
"posting_date": posting_date or today(),
"company": company,
"fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0],
"cost_center": cost_center,
"closing_account_head": surplus_account,
"remarks": "test",
}
)
pcv.insert()
if submit:
pcv.submit()
return pcv

View File

@@ -419,7 +419,8 @@
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate"
"label": "Rate",
"options": "currency"
},
{
"default": "0",
@@ -647,7 +648,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2024-05-17 13:16:34.496704",
"modified": "2024-09-16 18:14:51.314765",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
@@ -709,4 +710,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -5,6 +5,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
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
@@ -14,7 +15,7 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.get_item_details import get_item_details
class TestPricingRule(unittest.TestCase):
class TestPricingRule(FrappeTestCase):
def setUp(self):
delete_existing_pricing_rules()
setup_pricing_rule_data()

View File

@@ -648,11 +648,11 @@ frappe.ui.form.on("Purchase Invoice", {
},
onload: function (frm) {
if (frm.doc.__onload && frm.is_new()) {
if (frm.doc.supplier) {
if (frm.doc.__onload && frm.doc.supplier) {
if (frm.is_new()) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
if (!frm.doc.__onload.enable_apply_tds) {
if (!frm.doc.__onload.supplier_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
}
}

View File

@@ -1271,6 +1271,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
@@ -1630,7 +1631,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-07-25 19:42:36.931278",
"modified": "2024-09-11 12:59:19.130593",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -346,22 +346,6 @@ class PurchaseInvoice(BuyingController):
self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", tds_category)
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
po = qb.DocType("Purchase Order")
po_with_tds = (
qb.from_(po)
.select(po.name)
.where(
po.docstatus.eq(1)
& (po.name.isin(purchase_orders))
& (po.apply_tds.eq(1))
& (po.tax_withholding_category.notnull())
)
.run()
)
self.set_onload("enable_apply_tds", True if po_with_tds else False)
super().set_missing_values(for_validate)
def validate_credit_to_acc(self):

View File

@@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
if not cost_center_allocation:
return gl_map
new_gl_map = []
for d in gl_map:
cost_center = d.get("cost_center")
# Validate budget against main cost center
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
if cost_center and cost_center_allocation.get(cost_center):
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
new_gl_map.append(gle)
else:
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
for sub_cost_center, percentage in cost_center_allocation:
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
new_gl_map.append(gle)
return new_gl_map
def get_cost_center_allocation_data(company, posting_date):
par = frappe.qb.DocType("Cost Center Allocation")
child = frappe.qb.DocType("Cost Center Allocation Percentage")
def get_cost_center_allocation_data(company, posting_date, cost_center):
cost_center_allocation = frappe.db.get_value(
"Cost Center Allocation",
{
"docstatus": 1,
"company": company,
"valid_from": ("<=", posting_date),
"main_cost_center": cost_center,
},
pluck="name",
order_by="valid_from desc",
)
records = (
frappe.qb.from_(par)
.inner_join(child)
.on(par.name == child.parent)
.select(par.main_cost_center, child.cost_center, child.percentage)
.where(par.docstatus == 1)
.where(par.company == company)
.where(par.valid_from <= posting_date)
.orderby(par.valid_from, order=frappe.qb.desc)
).run(as_dict=True)
if not cost_center_allocation:
return []
cc_allocation = frappe._dict()
for d in records:
cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage)
records = frappe.db.get_all(
"Cost Center Allocation Percentage",
{"parent": cost_center_allocation},
["cost_center", "percentage"],
as_list=True,
)
return cc_allocation
return records
def merge_similar_entries(gl_map, precision=None):

View File

@@ -232,9 +232,15 @@ def get_group_by_asset_data(filters):
def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
finance_book_filter = ""
if filters.get("finance_book"):
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
# nosemgrep
return frappe.db.sql(
"""
f"""
SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
@@ -264,7 +270,14 @@ def get_assets_for_grouped_by_category(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.debit != 0
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition} {finance_book_filter}
group by a.asset_category
union
SELECT a.asset_category,
@@ -280,11 +293,16 @@ def get_assets_for_grouped_by_category(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.asset_category) as results
group by results.asset_category
""".format(condition),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"finance_book": filters.get("finance_book", ""),
},
as_dict=1,
)
@@ -292,9 +310,15 @@ def get_assets_for_grouped_by_category(filters):
def get_assets_for_grouped_by_asset(filters):
condition = ""
if filters.get("asset"):
condition = " and a.name = '{}'".format(filters.get("asset"))
condition = f" and a.name = '{filters.get('asset')}'"
finance_book_filter = ""
if filters.get("finance_book"):
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
# nosemgrep
return frappe.db.sql(
"""
f"""
SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
@@ -324,7 +348,14 @@ def get_assets_for_grouped_by_asset(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.debit != 0
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{finance_book_filter} {condition}
group by a.name
union
SELECT a.name as name,
@@ -340,11 +371,16 @@ def get_assets_for_grouped_by_asset(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.name) as results
group by results.name
""".format(condition),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"finance_book": filters.get("finance_book", ""),
},
as_dict=1,
)

View File

@@ -665,6 +665,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# will work as update after submit
journal_entry.flags.ignore_validate_update_after_submit = True
# Ledgers will be reposted by Reconciliation tool
journal_entry.flags.ignore_reposting_on_reconciliation = True
if not do_not_save:
journal_entry.save(ignore_permissions=True)
@@ -745,40 +747,74 @@ def cancel_exchange_gain_loss_journal(
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
journals = frappe.db.get_all(
"Journal Entry Account",
filters={
"reference_type": parent_doc.doctype,
"reference_name": parent_doc.name,
"docstatus": 1,
},
fields=["parent"],
as_list=1,
gain_loss_journals = get_linked_exchange_gain_loss_journal(
referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1
)
if journals:
gain_loss_journals = frappe.db.get_all(
"Journal Entry",
filters={
"name": ["in", [x[0] for x in journals]],
"voucher_type": "Exchange Gain Or Loss",
"docstatus": 1,
},
as_list=1,
)
for doc in gain_loss_journals:
gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
if referenced_dt and referenced_dn:
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
if (
len(references) == 2
and (referenced_dt, referenced_dn) in references
and (parent_doc.doctype, parent_doc.name) in references
):
# only cancel JE generated against parent_doc and referenced_dn
gain_loss_je.cancel()
else:
for doc in gain_loss_journals:
gain_loss_je = frappe.get_doc("Journal Entry", doc)
if referenced_dt and referenced_dn:
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
if (
len(references) == 2
and (referenced_dt, referenced_dn) in references
and (parent_doc.doctype, parent_doc.name) in references
):
# only cancel JE generated against parent_doc and referenced_dn
gain_loss_je.cancel()
else:
gain_loss_je.cancel()
def delete_exchange_gain_loss_journal(
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
) -> None:
"""
Delete Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
gain_loss_journals = get_linked_exchange_gain_loss_journal(
referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=2
)
for doc in gain_loss_journals:
gain_loss_je = frappe.get_doc("Journal Entry", doc)
if referenced_dt and referenced_dn:
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
if (
len(references) == 2
and (referenced_dt, referenced_dn) in references
and (parent_doc.doctype, parent_doc.name) in references
):
# only delete JE generated against parent_doc and referenced_dn
gain_loss_je.delete()
else:
gain_loss_je.delete()
def get_linked_exchange_gain_loss_journal(referenced_dt: str, referenced_dn: str, je_docstatus: int) -> list:
"""
Get all the linked exchange gain/loss journal entries for a given document.
"""
gain_loss_journals = []
if journals := frappe.db.get_all(
"Journal Entry Account",
{
"reference_type": referenced_dt,
"reference_name": referenced_dn,
"docstatus": je_docstatus,
},
pluck="parent",
):
gain_loss_journals = frappe.db.get_all(
"Journal Entry",
{
"name": ["in", journals],
"voucher_type": "Exchange Gain Or Loss",
"is_system_generated": 1,
"docstatus": je_docstatus,
},
pluck="name",
)
return gain_loss_journals
def cancel_common_party_journal(self):

View File

@@ -623,6 +623,9 @@ class Asset(AccountsController):
return records
def validate_make_gl_entry(self):
if self.is_composite_asset:
return True
purchase_document = self.get_purchase_document()
if not purchase_document:
return False

View File

@@ -65,6 +65,31 @@ frappe.ui.form.on("Purchase Order", {
}
},
supplier: function (frm) {
// Do not update if inter company reference is there as the details will already be updated
if (frm.updating_party_details || frm.doc.inter_company_invoice_reference) return;
if (frm.doc.__onload && frm.doc.__onload.load_after_mapping) return;
erpnext.utils.get_party_details(
frm,
"erpnext.accounts.party.get_party_details",
{
posting_date: frm.doc.transaction_date,
bill_date: frm.doc.bill_date,
party: frm.doc.supplier,
party_type: "Supplier",
account: frm.doc.credit_to,
price_list: frm.doc.buying_price_list,
fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template),
},
function () {
frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1);
frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1);
}
);
},
get_materials_from_supplier: function (frm) {
let po_details = [];
@@ -108,6 +133,15 @@ frappe.ui.form.on("Purchase Order", {
frm.set_value("transaction_date", frappe.datetime.get_today());
}
if (frm.doc.__onload && frm.doc.supplier) {
if (frm.is_new()) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
if (!frm.doc.__onload.supplier_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
}
}
erpnext.queries.setup_queries(frm, "Warehouse", function () {
return erpnext.queries.warehouse(frm.doc);
});

View File

@@ -648,6 +648,13 @@ class PurchaseOrder(BuyingController):
if sco:
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
def set_missing_values(self, for_validate=False):
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
self.set_onload("supplier_tds", tds_category)
super().set_missing_values(for_validate)
@frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
@@ -760,6 +767,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
def postprocess(source, target):
target.flags.ignore_permissions = ignore_permissions
set_missing_values(source, target)
# set tax_withholding_category from Purchase Order
if source.apply_tds and source.tax_withholding_category and target.apply_tds:
target.tax_withholding_category = source.tax_withholding_category
# Get the advance paid Journal Entries in Purchase Invoice Advance
if target.get("allocate_advances_automatically"):
target.set_advances()

View File

@@ -346,12 +346,17 @@ class AccountsController(TransactionBase):
repost_doc.save(ignore_permissions=True)
def on_trash(self):
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle()
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
# delete linked exchange gain/loss journal
delete_exchange_gain_loss_journal(self)
ple = frappe.qb.DocType("Payment Ledger Entry")
frappe.qb.from_(ple).delete().where(
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)

View File

@@ -640,7 +640,7 @@ class SellingController(StockController):
if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items:
item.gross_profit = flt(
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty),
((flt(item.stock_uom_rate) - flt(item.valuation_rate)) * item.stock_qty),
self.precision("amount", item),
)

View File

@@ -0,0 +1,170 @@
import json
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@frappe.whitelist()
def create_custom_fields_for_frappe_crm():
frappe.only_for("System Manager")
custom_fields = {
"Quotation": [
{
"fieldname": "crm_deal",
"fieldtype": "Data",
"label": "Frappe CRM Deal",
"insert_after": "party_name",
}
],
"Customer": [
{
"fieldname": "crm_deal",
"fieldtype": "Data",
"label": "Frappe CRM Deal",
"insert_after": "prospect_name",
}
],
}
create_custom_fields(custom_fields, ignore_validate=True)
@frappe.whitelist()
def create_prospect_against_crm_deal():
frappe.only_for("System Manager")
doc = frappe.form_dict
prospect = frappe.get_doc(
{
"doctype": "Prospect",
"company_name": doc.organization or doc.lead_name,
"no_of_employees": doc.no_of_employees,
"prospect_owner": doc.deal_owner,
"company": doc.erpnext_company,
"crm_deal": doc.crm_deal,
"territory": doc.territory,
"industry": doc.industry,
"website": doc.website,
"annual_revenue": doc.annual_revenue,
}
)
try:
prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name})
if not prospect_name:
prospect.insert()
prospect_name = prospect.name
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Error while creating prospect against CRM Deal: {frappe.form_dict.get('crm_deal_id')}",
)
pass
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
create_address("Prospect", prospect_name, doc.address)
frappe.response["message"] = prospect_name
def create_contacts(contacts, organization=None, link_doctype=None, link_docname=None):
for c in contacts:
c = frappe._dict(c)
existing_contact = contact_exists(c.email, c.mobile_no)
if existing_contact:
contact = frappe.get_doc("Contact", existing_contact)
else:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": c.get("full_name"),
"gender": c.get("gender"),
"company_name": organization,
}
)
if c.get("email"):
contact.append("email_ids", {"email_id": c.get("email"), "is_primary": 1})
if c.get("mobile_no"):
contact.append("phone_nos", {"phone": c.get("mobile_no"), "is_primary_mobile_no": 1})
link_doc(contact, link_doctype, link_docname)
contact.save(ignore_permissions=True)
def create_address(doctype, docname, address):
if not address:
return
try:
_address = frappe.db.exists("Address", address.get("name"))
if not _address:
new_address_doc = frappe.new_doc("Address")
for field in [
"address_title",
"address_type",
"address_line1",
"address_line2",
"city",
"state",
"pincode",
"country",
]:
if address.get(field):
new_address_doc.set(field, address.get(field))
new_address_doc.append("links", {"link_doctype": doctype, "link_name": docname})
new_address_doc.insert(ignore_mandatory=True)
return new_address_doc.name
else:
address = frappe.get_doc("Address", _address)
link_doc(address, doctype, docname)
address.save(ignore_permissions=True)
return address.name
except Exception:
frappe.log_error(frappe.get_traceback(), f"Error while creating address for {docname}")
def link_doc(doc, link_doctype, link_docname):
already_linked = any(
[(link.link_doctype == link_doctype and link.link_name == link_docname) for link in doc.links]
)
if not already_linked:
doc.append(
"links", {"link_doctype": link_doctype, "link_name": link_docname, "link_title": link_docname}
)
def contact_exists(email, mobile_no):
email_exist = frappe.db.exists("Contact Email", {"email_id": email})
mobile_exist = frappe.db.exists("Contact Phone", {"phone": mobile_no})
doctype = "Contact Email" if email_exist else "Contact Phone"
name = email_exist or mobile_exist
if name:
return frappe.db.get_value(doctype, name, "parent")
return False
@frappe.whitelist()
def create_customer(customer_data=None):
frappe.only_for("System Manager")
if not customer_data:
customer_data = frappe.form_dict
try:
customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")})
if not customer_name:
customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert(
ignore_permissions=True
)
customer_name = customer.name
contacts = json.loads(customer_data.get("contacts"))
create_contacts(contacts, customer_name, "Customer", customer_name)
create_address("Customer", customer_name, customer_data.get("address"))
return customer_name
except Exception:
frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal")
pass

View File

@@ -30,6 +30,9 @@ def update_report_json(report):
report_json = json.loads(report.json)
report_filter = report_json.get("filters")
if not report_filter:
return
keys_to_pop = [key for key in report_filter if key.startswith("range")]
report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop)

View File

@@ -213,6 +213,13 @@ class Project(Document):
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
def update_percent_complete(self):
if self.status == "Completed":
if (
len(frappe.get_all("Task", dict(project=self.name))) == 0
): # A project without tasks should be able to complete
self.percent_complete_method = "Manual"
self.percent_complete = 100
if self.percent_complete_method == "Manual":
if self.status == "Completed":
self.percent_complete = 100

View File

@@ -199,6 +199,34 @@ class TestProject(FrappeTestCase):
if not pt.is_group:
self.assertIsNotNone(pt.parent_task)
def test_project_having_no_tasks_complete(self):
project_name = "Test Project - No Tasks Completion"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc("Project", project_name)
project = frappe.get_doc(
{
"doctype": "Project",
"project_name": project_name,
"status": "Open",
"expected_start_date": nowdate(),
"company": "_Test Company",
}
).insert()
tasks = frappe.get_all(
"Task",
["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"],
dict(project=project.name),
order_by="creation asc",
)
self.assertEqual(project.status, "Open")
self.assertEqual(len(tasks), 0)
project.status = "Completed"
project.save()
self.assertEqual(project.status, "Completed")
def get_project(name, template):
project = frappe.get_doc(

View File

@@ -1502,6 +1502,31 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
batch_no(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.use_serial_batch_fields && row.batch_no) {
var params = this._get_args(row);
params.batch_no = row.batch_no;
params.uom = row.uom;
frappe.call({
method: "erpnext.stock.get_item_details.get_batch_based_item_price",
args: {
params: params,
item_code: row.item_code,
},
callback: function(r) {
if (!r.exc && r.message) {
row.price_list_rate = r.message;
row.rate = r.message;
refresh_field("rate", row.name, row.parentfield);
refresh_field("price_list_rate", row.name, row.parentfield);
}
}
})
}
}
toggle_item_grid_columns(company_currency) {
const me = this;
// toggle columns

View File

@@ -71,7 +71,7 @@ frappe.ui.form.on("Quotation", {
frm.trigger("set_label");
frm.trigger("toggle_reqd_lead_customer");
frm.trigger("set_dynamic_field_label");
frm.set_value("party_name", "");
// frm.set_value("party_name", ""); // removed to set party_name from url for crm integration
frm.set_value("customer_name", "");
},

View File

@@ -220,6 +220,10 @@ class Quotation(SellingController):
"Lead", self.party_name, ["lead_name", "company_name"]
)
self.customer_name = company_name or lead_name
elif self.party_name and self.quotation_to == "Prospect":
self.customer_name = self.party_name
elif self.party_name and self.quotation_to == "CRM Deal":
self.customer_name = frappe.db.get_value("CRM Deal", self.party_name, "organization")
def update_opportunity(self, status):
for opportunity in set(d.prevdoc_docname for d in self.get("items")):

View File

@@ -389,28 +389,14 @@ erpnext.PointOfSale.ItemCart = class {
placeholder: discount ? discount + "%" : __("Enter discount percentage."),
input_class: "input-xs",
onchange: function () {
if (flt(this.value) != 0) {
frappe.model.set_value(
frm.doc.doctype,
frm.doc.name,
"additional_discount_percentage",
flt(this.value)
);
me.hide_discount_control(this.value);
} else {
frappe.model.set_value(
frm.doc.doctype,
frm.doc.name,
"additional_discount_percentage",
0
);
me.$add_discount_elem.css({
border: "1px dashed var(--gray-500)",
padding: "var(--padding-sm) var(--padding-md)",
});
me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`);
me.discount_field = undefined;
}
this.value = flt(this.value);
frappe.model.set_value(
frm.doc.doctype,
frm.doc.name,
"additional_discount_percentage",
flt(this.value)
);
me.hide_discount_control(this.value);
},
},
parent: this.$add_discount_elem.find(".add-discount-field"),
@@ -421,9 +407,13 @@ erpnext.PointOfSale.ItemCart = class {
}
hide_discount_control(discount) {
if (!discount) {
this.$add_discount_elem.css({ padding: "0px", border: "none" });
this.$add_discount_elem.html(`<div class="add-discount-field"></div>`);
if (!flt(discount)) {
this.$add_discount_elem.css({
border: "1px dashed var(--gray-500)",
padding: "var(--padding-sm) var(--padding-md)",
});
this.$add_discount_elem.html(`${this.get_discount_icon()} ${__("Add Discount")}`);
this.discount_field = undefined;
} else {
this.$add_discount_elem.css({
border: "1px dashed var(--dark-green-500)",
@@ -1051,6 +1041,7 @@ erpnext.PointOfSale.ItemCart = class {
this.highlight_checkout_btn(false);
}
this.hide_discount_control(frm.doc.additional_discount_percentage);
this.update_totals_section(frm);
if (frm.doc.docstatus === 1) {

View File

@@ -149,7 +149,11 @@ class HolidayList(Document):
unique_dates = []
for row in self.holidays:
if row.holiday_date in unique_dates:
frappe.throw(_("Holiday Date {0} added multiple times").format(frappe.bold(row.holiday_date)))
frappe.throw(
_("Holiday Date {0} added multiple times").format(
frappe.bold(formatdate(row.holiday_date))
)
)
unique_dates.append(row.holiday_date)

View File

@@ -1,4 +1,4 @@
<div>
<a href={{ route }}>{{ title }}</a>
<a href={{ route }}>{{ title or name }}</a>
</div>
<!-- this is a sample default list template -->

View File

@@ -10,7 +10,7 @@ from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json
from erpnext import get_company_currency
from erpnext.accounts.doctype.pricing_rule.pricing_rule import (
@@ -889,7 +889,7 @@ def insert_item_price(args):
)
def get_item_price(args, item_code, ignore_party=False):
def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]:
"""
Get name, price_list_rate from Item Price based on conditions
Check if the desired qty is within the increment of the packing list.
@@ -906,13 +906,17 @@ def get_item_price(args, item_code, ignore_party=False):
(ip.item_code == item_code)
& (ip.price_list == args.get("price_list"))
& (IfNull(ip.uom, "").isin(["", args.get("uom")]))
& (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")]))
)
.orderby(ip.valid_from, order=frappe.qb.desc)
.orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
.orderby(ip.uom, order=frappe.qb.desc)
)
if force_batch_no:
query = query.where(ip.batch_no == args.get("batch_no"))
else:
query = query.where(IfNull(ip.batch_no, "").isin(["", args.get("batch_no")]))
if not ignore_party:
if args.get("customer"):
query = query.where(ip.customer == args.get("customer"))
@@ -930,6 +934,21 @@ def get_item_price(args, item_code, ignore_party=False):
return query.run()
@frappe.whitelist()
def get_batch_based_item_price(params, item_code) -> float:
if isinstance(params, str):
params = parse_json(params)
item_price = get_item_price(params, item_code, force_batch_no=True)
if not item_price:
item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True)
if item_price and item_price[0].uom == params.get("uom"):
return item_price[0].price_list_rate
return 0.0
def get_price_list_rate_for(args, item_code):
"""
:param customer: link to Customer DocType