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 = [ cost_centers = [
"Main Cost Center 1", "Main Cost Center 1",
"Main Cost Center 2", "Main Cost Center 2",
"Main Cost Center 3",
"Sub Cost Center 1", "Sub Cost Center 1",
"Sub Cost Center 2", "Sub Cost Center 2",
"Sub Cost Center 3",
] ]
for cc in cost_centers: for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company") create_cost_center(cost_center_name=cc, company="_Test Company")
@@ -36,7 +38,7 @@ class TestCostCenterAllocation(unittest.TestCase):
) )
jv = make_journal_entry( 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]] 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): def test_valid_from_based_on_existing_gle(self):
# GLE posted against Sub Cost Center 1 on today # GLE posted against Sub Cost Center 1 on today
jv = make_journal_entry( jv = make_journal_entry(
"_Test Cash - _TC", "Cash - _TC",
"Sales - _TC", "Sales - _TC",
100, 100,
cost_center="Main Cost Center 1 - _TC", cost_center="Main Cost Center 1 - _TC",
@@ -141,6 +143,53 @@ class TestCostCenterAllocation(unittest.TestCase):
jv.cancel() 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( def create_cost_center_allocation(
company, company,

View File

@@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
accounts_add(doc, cdt, cdn) { accounts_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn); var row = frappe.get_doc(cdt, cdn);
row.exchange_rate = 1;
$.each(doc.accounts, function (i, d) { $.each(doc.accounts, function (i, d) {
if (d.account && d.party && d.party_type) { if (d.account && d.party && d.party_type) {
row.account = d.account; row.account = d.account;
row.party = d.party; row.party = d.party;
row.party_type = d.party_type; row.party_type = d.party_type;
row.exchange_rate = d.exchange_rate;
} }
}); });
// set difference // set difference
if (doc.difference) { if (doc.difference) {
if (doc.difference > 0) { 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; row.credit = doc.difference;
} else { } else {
row.debit_in_account_currency = -doc.difference; row.debit_in_account_currency = -doc.difference / row.exchange_rate;
row.debit = -doc.difference; row.debit = -doc.difference;
} }
} }
@@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, {
callback: function (r) { callback: function (r) {
if (r.message) { if (r.message) {
$.extend(d, 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); erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn);
refresh_field("accounts"); 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() self.update_booked_depreciation()
def on_update_after_submit(self): 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": []}) self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
if self.needs_repost: if self.needs_repost:
self.validate_for_repost() self.validate_for_repost()

View File

@@ -385,7 +385,15 @@ frappe.ui.form.on("Payment Entry", {
payment_type: function (frm) { payment_type: function (frm) {
if (frm.doc.payment_type == "Internal Transfer") { if (frm.doc.payment_type == "Internal Transfer") {
$.each( $.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) { function (i, field) {
frm.set_value(field, null); frm.set_value(field, null);
} }

View File

@@ -1791,6 +1791,79 @@ class TestPaymentEntry(FrappeTestCase):
# 'Is Opening' should always be 'No' for normal advance payments # 'Is Opening' should always be 'No' for normal advance payments
self.assertEqual(gl_with_opening_set, []) 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): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings 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 import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry 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.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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account 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.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.stock.doctype.item.test_item import create_item 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.invoices), 1)
self.assertEqual(len(pr.payments), 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): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):
@@ -1872,3 +1945,61 @@ def make_supplier(supplier_name, currency=None):
return supplier.name return supplier.name
else: else:
return supplier_name 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\"", "depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate" "label": "Rate",
"options": "currency"
}, },
{ {
"default": "0", "default": "0",
@@ -647,7 +648,7 @@
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2024-05-17 13:16:34.496704", "modified": "2024-09-16 18:14:51.314765",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",
@@ -709,4 +710,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "title" "title_field": "title"
} }

View File

@@ -5,6 +5,7 @@
import unittest import unittest
import frappe 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.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.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 from erpnext.stock.get_item_details import get_item_details
class TestPricingRule(unittest.TestCase): class TestPricingRule(FrappeTestCase):
def setUp(self): def setUp(self):
delete_existing_pricing_rules() delete_existing_pricing_rules()
setup_pricing_rule_data() setup_pricing_rule_data()

View File

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

View File

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

View File

@@ -346,22 +346,6 @@ class PurchaseInvoice(BuyingController):
self.tax_withholding_category = tds_category self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", 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) super().set_missing_values(for_validate)
def validate_credit_to_acc(self): 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): 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 = [] new_gl_map = []
for d in gl_map: for d in gl_map:
cost_center = d.get("cost_center") cost_center = d.get("cost_center")
# Validate budget against main cost center # Validate budget against main cost center
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)) validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
cost_center_allocation = get_cost_center_allocation_data(
if cost_center and cost_center_allocation.get(cost_center): gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items(): )
gle = copy.deepcopy(d) if not cost_center_allocation:
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:
new_gl_map.append(d) 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 return new_gl_map
def get_cost_center_allocation_data(company, posting_date): def get_cost_center_allocation_data(company, posting_date, cost_center):
par = frappe.qb.DocType("Cost Center Allocation") cost_center_allocation = frappe.db.get_value(
child = frappe.qb.DocType("Cost Center Allocation Percentage") "Cost Center Allocation",
{
"docstatus": 1,
"company": company,
"valid_from": ("<=", posting_date),
"main_cost_center": cost_center,
},
pluck="name",
order_by="valid_from desc",
)
records = ( if not cost_center_allocation:
frappe.qb.from_(par) return []
.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)
cc_allocation = frappe._dict() records = frappe.db.get_all(
for d in records: "Cost Center Allocation Percentage",
cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage) {"parent": cost_center_allocation},
["cost_center", "percentage"],
as_list=True,
)
return cc_allocation return records
def merge_similar_entries(gl_map, precision=None): 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): def get_assets_for_grouped_by_category(filters):
condition = "" condition = ""
if filters.get("asset_category"): 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( return frappe.db.sql(
""" f"""
SELECT results.asset_category, SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, 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, 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 aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on join `tabCompany` company on
company.name = %(company)s 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 group by a.asset_category
union union
SELECT a.asset_category, SELECT a.asset_category,
@@ -280,11 +293,16 @@ def get_assets_for_grouped_by_category(filters):
end), 0) as depreciation_eliminated_during_the_period, end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period 0 as depreciation_amount_during_the_period
from `tabAsset` a 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 a.asset_category) as results
group by results.asset_category 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, as_dict=1,
) )
@@ -292,9 +310,15 @@ def get_assets_for_grouped_by_category(filters):
def get_assets_for_grouped_by_asset(filters): def get_assets_for_grouped_by_asset(filters):
condition = "" condition = ""
if filters.get("asset"): 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( return frappe.db.sql(
""" f"""
SELECT results.name as asset, SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, 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, 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 aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on join `tabCompany` company on
company.name = %(company)s 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 group by a.name
union union
SELECT a.name as name, 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, end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period 0 as depreciation_amount_during_the_period
from `tabAsset` a 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 a.name) as results
group by results.name 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, 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 # will work as update after submit
journal_entry.flags.ignore_validate_update_after_submit = True 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: if not do_not_save:
journal_entry.save(ignore_permissions=True) 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. 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"]: if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
journals = frappe.db.get_all( gain_loss_journals = get_linked_exchange_gain_loss_journal(
"Journal Entry Account", referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1
filters={
"reference_type": parent_doc.doctype,
"reference_name": parent_doc.name,
"docstatus": 1,
},
fields=["parent"],
as_list=1,
) )
for doc in gain_loss_journals:
if journals: gain_loss_je = frappe.get_doc("Journal Entry", doc)
gain_loss_journals = frappe.db.get_all( if referenced_dt and referenced_dn:
"Journal Entry", references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
filters={ if (
"name": ["in", [x[0] for x in journals]], len(references) == 2
"voucher_type": "Exchange Gain Or Loss", and (referenced_dt, referenced_dn) in references
"docstatus": 1, and (parent_doc.doctype, parent_doc.name) in references
}, ):
as_list=1, # only cancel JE generated against parent_doc and referenced_dn
)
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:
gain_loss_je.cancel() 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): def cancel_common_party_journal(self):

View File

@@ -623,6 +623,9 @@ class Asset(AccountsController):
return records return records
def validate_make_gl_entry(self): def validate_make_gl_entry(self):
if self.is_composite_asset:
return True
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
if not purchase_document: if not purchase_document:
return False 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) { get_materials_from_supplier: function (frm) {
let po_details = []; let po_details = [];
@@ -108,6 +133,15 @@ frappe.ui.form.on("Purchase Order", {
frm.set_value("transaction_date", frappe.datetime.get_today()); 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 () { erpnext.queries.setup_queries(frm, "Warehouse", function () {
return erpnext.queries.warehouse(frm.doc); return erpnext.queries.warehouse(frm.doc);
}); });

View File

@@ -648,6 +648,13 @@ class PurchaseOrder(BuyingController):
if sco: if sco:
update_sco_status(sco, "Closed" if self.status == "Closed" else None) 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 @frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): 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): def postprocess(source, target):
target.flags.ignore_permissions = ignore_permissions target.flags.ignore_permissions = ignore_permissions
set_missing_values(source, target) 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 # Get the advance paid Journal Entries in Purchase Invoice Advance
if target.get("allocate_advances_automatically"): if target.get("allocate_advances_automatically"):
target.set_advances() target.set_advances()

View File

@@ -346,12 +346,17 @@ class AccountsController(TransactionBase):
repost_doc.save(ignore_permissions=True) repost_doc.save(ignore_permissions=True)
def on_trash(self): def on_trash(self):
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
self._remove_references_in_repost_doctypes() self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile() self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle() self.remove_serial_and_batch_bundle()
# delete sl and gl entries on deletion of transaction # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): 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") ple = frappe.qb.DocType("Payment Ledger Entry")
frappe.qb.from_(ple).delete().where( frappe.qb.from_(ple).delete().where(
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) (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"]: if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items: for item in self.items:
item.gross_profit = flt( 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), 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_json = json.loads(report.json)
report_filter = report_json.get("filters") report_filter = report_json.get("filters")
if not report_filter:
return
keys_to_pop = [key for key in report_filter if key.startswith("range")] 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) 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", "") frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
def update_percent_complete(self): 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.percent_complete_method == "Manual":
if self.status == "Completed": if self.status == "Completed":
self.percent_complete = 100 self.percent_complete = 100

View File

@@ -199,6 +199,34 @@ class TestProject(FrappeTestCase):
if not pt.is_group: if not pt.is_group:
self.assertIsNotNone(pt.parent_task) 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): def get_project(name, template):
project = frappe.get_doc( 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) { toggle_item_grid_columns(company_currency) {
const me = this; const me = this;
// toggle columns // toggle columns

View File

@@ -71,7 +71,7 @@ frappe.ui.form.on("Quotation", {
frm.trigger("set_label"); frm.trigger("set_label");
frm.trigger("toggle_reqd_lead_customer"); frm.trigger("toggle_reqd_lead_customer");
frm.trigger("set_dynamic_field_label"); 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", ""); frm.set_value("customer_name", "");
}, },

View File

@@ -220,6 +220,10 @@ class Quotation(SellingController):
"Lead", self.party_name, ["lead_name", "company_name"] "Lead", self.party_name, ["lead_name", "company_name"]
) )
self.customer_name = company_name or lead_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): def update_opportunity(self, status):
for opportunity in set(d.prevdoc_docname for d in self.get("items")): 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."), placeholder: discount ? discount + "%" : __("Enter discount percentage."),
input_class: "input-xs", input_class: "input-xs",
onchange: function () { onchange: function () {
if (flt(this.value) != 0) { this.value = flt(this.value);
frappe.model.set_value( frappe.model.set_value(
frm.doc.doctype, frm.doc.doctype,
frm.doc.name, frm.doc.name,
"additional_discount_percentage", "additional_discount_percentage",
flt(this.value) flt(this.value)
); );
me.hide_discount_control(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;
}
}, },
}, },
parent: this.$add_discount_elem.find(".add-discount-field"), parent: this.$add_discount_elem.find(".add-discount-field"),
@@ -421,9 +407,13 @@ erpnext.PointOfSale.ItemCart = class {
} }
hide_discount_control(discount) { hide_discount_control(discount) {
if (!discount) { if (!flt(discount)) {
this.$add_discount_elem.css({ padding: "0px", border: "none" }); this.$add_discount_elem.css({
this.$add_discount_elem.html(`<div class="add-discount-field"></div>`); 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 { } else {
this.$add_discount_elem.css({ this.$add_discount_elem.css({
border: "1px dashed var(--dark-green-500)", border: "1px dashed var(--dark-green-500)",
@@ -1051,6 +1041,7 @@ erpnext.PointOfSale.ItemCart = class {
this.highlight_checkout_btn(false); this.highlight_checkout_btn(false);
} }
this.hide_discount_control(frm.doc.additional_discount_percentage);
this.update_totals_section(frm); this.update_totals_section(frm);
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {

View File

@@ -149,7 +149,11 @@ class HolidayList(Document):
unique_dates = [] unique_dates = []
for row in self.holidays: for row in self.holidays:
if row.holiday_date in unique_dates: 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) unique_dates.append(row.holiday_date)

View File

@@ -1,4 +1,4 @@
<div> <div>
<a href={{ route }}>{{ title }}</a> <a href={{ route }}>{{ title or name }}</a>
</div> </div>
<!-- this is a sample default list template --> <!-- 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.meta import get_field_precision
from frappe.model.utils import get_fetch_values from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import IfNull, Sum 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 import get_company_currency
from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( 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 Get name, price_list_rate from Item Price based on conditions
Check if the desired qty is within the increment of the packing list. 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.item_code == item_code)
& (ip.price_list == args.get("price_list")) & (ip.price_list == args.get("price_list"))
& (IfNull(ip.uom, "").isin(["", args.get("uom")])) & (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(ip.valid_from, order=frappe.qb.desc)
.orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc) .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
.orderby(ip.uom, 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 not ignore_party:
if args.get("customer"): if args.get("customer"):
query = query.where(ip.customer == 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() 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): def get_price_list_rate_for(args, item_code):
""" """
:param customer: link to Customer DocType :param customer: link to Customer DocType