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

chore: release v14
This commit is contained in:
ruthra kumar
2024-09-18 13:01:22 +05:30
committed by GitHub
10 changed files with 253 additions and 95 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

@@ -1365,6 +1365,79 @@ class TestPaymentEntry(FrappeTestCase):
expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps)) expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps))
self.assertEqual(out_str, expected_out_str) self.assertEqual(out_str, expected_out_str)
@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

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

@@ -1265,6 +1265,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
}, },
@@ -1610,7 +1611,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

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

@@ -703,40 +703,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

@@ -326,11 +326,16 @@ 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()
# 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

@@ -591,7 +591,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

@@ -385,28 +385,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"),
@@ -417,9 +403,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)",
@@ -1044,6 +1034,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) {