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

chore: release v15
This commit is contained in:
ruthra kumar
2025-04-22 19:16:15 +05:30
committed by GitHub
52 changed files with 838 additions and 281 deletions

View File

@@ -1,5 +1,5 @@
exclude: 'node_modules|.git'
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: false

View File

@@ -3300,26 +3300,25 @@ def set_paid_amount_and_received_amount(
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
else:
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if payment_type == "Receive":
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
if bank and company_currency != bank.account_currency:
received_amount = paid_amount / doc.get("conversion_rate", 1)
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
# settings if it is for receive
paid_amount = abs(outstanding_amount)
if bank_amount:
received_amount = bank_amount
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if bank and company_currency != bank.account_currency:
# doc currency can be different from bank currency
posting_date = doc.get("posting_date") or doc.get("transaction_date")
conversion_rate = get_exchange_rate(
bank.account_currency, party_account_currency, posting_date
)
received_amount = paid_amount / conversion_rate
else:
if bank and company_currency != bank.account_currency:
paid_amount = received_amount / doc.get("conversion_rate", 1)
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
received_amount = paid_amount * doc.get("conversion_rate", 1)
# if payment type is pay, then paid amount and received amount are swapped
if payment_type == "Pay":
paid_amount, received_amount = received_amount, paid_amount
return paid_amount, received_amount

View File

@@ -37,6 +37,7 @@
"column_break_19",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"section_break1",
"rate",
@@ -847,11 +848,17 @@
{
"fieldname": "column_break_ciit",
"fieldtype": "Column Break"
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
}
],
"istable": 1,
"links": [],
"modified": "2024-05-07 15:56:53.343317",
"modified": "2024-05-07 15:56:54.343317",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -39,6 +39,7 @@ class POSInvoiceItem(Document):
description: DF.TextEditor
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
dn_detail: DF.Data | None
enable_deferred_revenue: DF.Check
expense_account: DF.Link | None

View File

@@ -2688,13 +2688,13 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
To test if after applying discount on grand total,
the grand total is calculated correctly without any rounding errors
"""
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
invoice = make_purchase_invoice(qty=3, rate=100, do_not_save=True, do_not_submit=True)
invoice.append(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 21.39,
"qty": 3,
"rate": 50.3,
},
)
invoice.append(
@@ -2703,18 +2703,19 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"rate": 15.5,
"rate": 15,
},
)
# the grand total here will be 255.71
# the grand total here will be 518.54
invoice.disable_rounded_total = 1
# apply discount on grand total to adjust the grand total to 255
invoice.discount_amount = 0.71
# apply discount on grand total to adjust the grand total to 518
invoice.discount_amount = 0.54
invoice.save()
# check if grand total is 496 and not something like 254.99 due to rounding errors
self.assertEqual(invoice.grand_total, 255)
# check if grand total is 518 and not something like 517.99 due to rounding errors
self.assertEqual(invoice.grand_total, 518)
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
"""

View File

@@ -38,6 +38,7 @@
"column_break_30",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"sec_break2",
"rate",
@@ -840,7 +841,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "section_break_26",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -971,12 +972,18 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-12 16:33:12.453290",
"modified": "2025-03-12 16:33:13.453290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -34,6 +34,7 @@ class PurchaseInvoiceItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
enable_deferred_expense: DF.Check
expense_account: DF.Link | None
from_warehouse: DF.Link | None

View File

@@ -196,7 +196,7 @@
"fieldname": "item_wise_tax_detail",
"fieldtype": "Code",
"hidden": 1,
"label": "Item Wise Tax Detail ",
"label": "Item Wise Tax Detail",
"oldfieldname": "item_wise_tax_detail",
"oldfieldtype": "Small Text",
"print_hide": 1,
@@ -235,10 +235,11 @@
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-04-08 19:51:36.678551",
"modified": "2025-04-15 13:14:48.936047",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -782,24 +782,6 @@ frappe.ui.form.on("Sales Invoice", {
};
};
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
debounce: 2000,
callback: function (r) {
if (r.message) {
frm.set_value("company_address", r.message);
} else {
frm.set_value("company_address", "");
}
},
});
}
},
onload: function (frm) {
frm.redemption_conversion_factor = null;
},

View File

@@ -37,6 +37,7 @@
"column_break_19",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"section_break1",
"rate",
@@ -259,7 +260,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -932,6 +933,12 @@
"fieldname": "column_break_ytgd",
"fieldtype": "Column Break"
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
@@ -976,7 +983,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-12 16:33:52.503777",
"modified": "2025-03-12 16:33:55.503777",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -40,6 +40,7 @@ class SalesInvoiceItem(Document):
discount_account: DF.Link | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
dn_detail: DF.Data | None
enable_deferred_revenue: DF.Check
expense_account: DF.Link | None

View File

@@ -100,7 +100,7 @@ class PartyLedgerSummaryReport:
conditions.append(doctype.territory.isin(self.filters.territory))
if self.filters.get(group_field):
conditions.append(doctype.get(group_field).isin(self.filters.get(group_field)))
conditions.append(doctype[group_field].isin(self.filters.get(group_field)))
if self.filters.payment_terms_template:
conditions.append(doctype.payment_terms == self.filters.payment_terms_template)

View File

@@ -59,3 +59,33 @@ class TestSupplierLedgerSummary(FrappeTestCase, AccountsTestMixin):
for field in expected:
with self.subTest(field=field):
self.assertEqual(report_output[0].get(field), expected.get(field))
def test_supplier_ledger_summary_with_filters(self):
self.create_purchase_invoice()
supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group")
filters = {
"company": self.company,
"from_date": today(),
"to_date": today(),
"supplier_group": supplier_group,
}
expected = {
"party": "_Test Supplier",
"party_name": "_Test Supplier",
"opening_balance": 0,
"invoiced_amount": 300.0,
"paid_amount": 0,
"return_amount": 0,
"closing_balance": 300.0,
"currency": "INR",
"supplier_name": "_Test Supplier",
}
report_output = execute(filters)[1]
self.assertEqual(len(report_output), 1)
for field in expected:
with self.subTest(field=field):
self.assertEqual(report_output[0].get(field), expected.get(field))

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.utils import getdate
def execute(filters=None):
@@ -33,6 +34,7 @@ def execute(filters=None):
def validate_filters(filters):
"""Validate if dates are properly set"""
filters = frappe._dict(filters or {})
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
@@ -68,7 +70,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
if net_total_map.get((voucher_type, name)):
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
@@ -435,12 +437,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
def get_tax_rate_map(filters):
rate_map = frappe.get_all(
"Tax Withholding Rate",
filters={
"from_date": ("<=", filters.get("from_date")),
"to_date": (">=", filters.get("to_date")),
},
fields=["parent", "tax_withholding_rate"],
as_list=1,
filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
)
return frappe._dict(rate_map)
rate_list = frappe._dict()
for rate in rate_map:
rate_list.setdefault(rate.parent, []).append(frappe._dict(rate))
return rate_list
def get_tax_withholding_rates(tax_withholding, posting_date):
# returns the row that matches with the fiscal year from posting date
for rate in tax_withholding:
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
return rate.tax_withholding_rate
return 0

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
from frappe.utils import add_to_date, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -60,6 +60,56 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
]
self.check_expected_values(result, expected_values)
def test_date_filters_in_multiple_tax_withholding_rules(self):
create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1)
# insert new rate in same fiscal year
fiscal_year = get_fiscal_year(today(), company="_Test Company")
mid_year = add_to_date(fiscal_year[1], months=6)
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
tds_doc.rates[0].to_date = mid_year
tds_doc.append(
"rates",
{
"tax_withholding_rate": 20,
"from_date": add_to_date(mid_year, days=1),
"to_date": fiscal_year[2],
"single_threshold": 1,
"cumulative_threshold": 1,
},
)
tds_doc.save()
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1.apply_tds = 1
inv_1.tax_withholding_category = "TDS - 3"
inv_1.submit()
inv_2 = make_purchase_invoice(
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
)
inv_2.set_posting_time = 1
inv_1.apply_tds = 1
inv_2.tax_withholding_category = "TDS - 3"
inv_2.save()
inv_2.submit()
result = execute(
frappe._dict(
company="_Test Company",
party_type="Supplier",
from_date=fiscal_year[1],
to_date=fiscal_year[2],
)
)[1]
expected_values = [
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
]
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
for i in range(len(result)):
voucher = frappe._dict(result[i])

View File

@@ -43,6 +43,7 @@
"column_break_28",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"sec_break2",
"rate",
@@ -781,7 +782,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin_section",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -911,6 +912,12 @@
"fieldname": "column_break_fyqr",
"fieldtype": "Column Break"
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"allow_on_submit": 1,
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
@@ -927,7 +934,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-13 17:27:43.468602",
"modified": "2025-03-13 17:27:44.468602",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -37,6 +37,7 @@ class PurchaseOrderItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
expected_delivery_date: DF.Date | None
expense_account: DF.Link | None
fg_item: DF.Link | None

View File

@@ -154,9 +154,31 @@ frappe.ui.form.on("Request for Quotation", {
);
frm.page.set_inner_btn_group_as_primary(__("Create"));
frm.add_custom_button(
__("Supplier Quotation Comparison"),
function () {
frm.trigger("show_supplier_quotation_comparison");
},
__("View")
);
}
},
show_supplier_quotation_comparison(frm) {
const today = new Date();
const oneMonthAgo = new Date(today);
oneMonthAgo.setMonth(today.getMonth() - 1);
frappe.route_options = {
company: frm.doc.company,
from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
to_date: moment(today).format("YYYY-MM-DD"),
request_for_quotation: frm.doc.name,
};
frappe.set_route("query-report", "Supplier Quotation Comparison");
},
make_supplier_quotation: function (frm) {
var doc = frm.doc;
var dialog = new frappe.ui.Dialog({

View File

@@ -32,6 +32,7 @@
"price_list_rate",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"col_break_price_list",
"base_price_list_rate",
"sec_break1",
@@ -565,13 +566,19 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-17 12:25:26.235367",
"modified": "2024-06-02 06:22:18.864822",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation Item",

View File

@@ -26,6 +26,7 @@ class SupplierQuotationItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
expected_delivery_date: DF.Date | None
image: DF.Attach | None
is_free_item: DF.Check

View File

@@ -1841,8 +1841,11 @@ class AccountsController(TransactionBase):
and self.get("discount_amount")
and self.get("additional_discount_account")
):
amount = item.amount
base_amount = item.base_amount
amount += item.distributed_discount_amount
base_amount += flt(
item.distributed_discount_amount * self.get("conversion_rate"),
item.precision("distributed_discount_amount"),
)
return amount, base_amount
@@ -2395,13 +2398,12 @@ class AccountsController(TransactionBase):
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
)
d.outstanding = d.payment_amount
d.base_outstanding = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
)
d.base_outstanding = d.base_payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
d.base_outstanding = d.base_payment_amount
else:
self.fetch_payment_terms_from_order(
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms

View File

@@ -8,9 +8,10 @@ from collections import OrderedDict, defaultdict
import frappe
from frappe import qb, scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.permissions import has_permission
from frappe.query_builder import Criterion, CustomFunction
from frappe.query_builder.functions import Concat, Locate, Sum
from frappe.utils import nowdate, today, unique
from frappe.utils import cint, nowdate, today, unique
from pypika import Order
import erpnext
@@ -20,10 +21,28 @@ from erpnext.stock.get_item_details import _get_item_tax_template
# searches for active employees
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def employee_query(doctype, txt, searchfield, start, page_len, filters):
def employee_query(
doctype,
txt,
searchfield,
start,
page_len,
filters,
reference_doctype: str | None = None,
ignore_user_permissions: bool = False,
):
doctype = "Employee"
conditions = []
fields = get_fields(doctype, ["name", "employee_name"])
ignore_permissions = False
if reference_doctype and ignore_user_permissions:
ignore_permissions = has_ignored_field(reference_doctype, doctype) and has_permission(
doctype,
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
)
mcond = "" if ignore_permissions else get_match_cond(doctype)
return frappe.db.sql(
"""select {fields} from `tabEmployee`
@@ -42,13 +61,32 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
"fields": ", ".join(fields),
"key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions),
"mcond": get_match_cond(doctype),
"mcond": mcond,
}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
)
def has_ignored_field(reference_doctype, doctype):
meta = frappe.get_meta(reference_doctype)
for field in meta.fields:
if not field.ignore_user_permissions:
continue
if field.fieldtype == "Link" and field.options == doctype:
return True
elif field.fieldtype == "Dynamic Link":
options = meta.get_link_doctype(field.fieldname)
if not options:
continue
if isinstance(options, str):
options = options.split("\n")
if doctype in options or "DocType" in options:
return True
return False
# searches for leads which are not converted
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -921,7 +959,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all(
"UOM",
filters={"name": ["like", f"%{txt}%"]},
filters={"name": ["like", f"%{txt}%"], "enabled": 1},
fields=["name"],
limit_start=start,
limit_page_length=page_len,

View File

@@ -84,8 +84,8 @@ status_map = {
"Delivery Note": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],

View File

@@ -988,7 +988,13 @@ class StockController(AccountsController):
def update_billing_percentage(self, update_modified=True):
target_ref_field = "amount"
if self.doctype == "Delivery Note":
target_ref_field = "amount - (returned_qty * rate)"
total_amount = total_returned = 0
for item in self.items:
total_amount += flt(item.amount)
total_returned += flt(item.returned_qty * item.rate)
if total_returned < total_amount:
target_ref_field = "(amount - (returned_qty * rate))"
self._update_percent_field(
{
@@ -1153,6 +1159,12 @@ class StockController(AccountsController):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return
self.__inter_company_reference = (
self.get("inter_company_reference")
if self.doctype == "Purchase Invoice"
else self.get("inter_company_invoice_reference")
)
item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
if not item_wise_transfer_qty:
return
@@ -1182,15 +1194,11 @@ class StockController(AccountsController):
bold(key[1]),
bold(flt(transferred_qty, precision)),
bold(parent_doctype),
get_link_to_form(parent_doctype, self.get("inter_company_reference")),
get_link_to_form(parent_doctype, self.__inter_company_reference),
)
)
def get_item_wise_inter_transfer_qty(self):
reference_field = "inter_company_reference"
if self.doctype == "Purchase Invoice":
reference_field = "inter_company_invoice_reference"
parent_doctype = {
"Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice",
@@ -1210,7 +1218,7 @@ class StockController(AccountsController):
child_tab.item_code,
child_tab.qty,
)
.where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
.where((parent_tab.name == self.__inter_company_reference) & (parent_tab.docstatus == 1))
)
data = query.run(as_dict=True)

View File

@@ -377,20 +377,22 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
# maintain actual tax rate based on idx
actual_tax_dict = dict(
[
[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
for tax in self.doc.get("taxes")
for tax in doc.taxes
if tax.charge_type == "Actual"
]
)
for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")):
for i, tax in enumerate(doc.taxes):
# tax_amount represents the amount of tax for the current step
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
if frappe.flags.round_row_wise_tax:
@@ -425,30 +427,39 @@ class calculate_taxes_and_totals:
tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount)
else:
tax.grand_total_for_current_item = flt(
self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount
doc.taxes[i - 1].grand_total_for_current_item + current_tax_amount
)
# set precision in the last item iteration
if n == len(self._items) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
discount_amount_applied = self.discount_amount_applied
if doc.apply_discount_on == "Grand Total" and (
discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage
):
tax_amount_precision = doc.taxes[0].precision("tax_amount")
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
for i, tax in enumerate(doc.taxes):
if discount_amount_applied:
tax.tax_amount_after_discount_amount = flt(
tax.tax_amount_after_discount_amount, tax_amount_precision
)
self._set_in_company_currency(tax, ["total"])
self.set_cumulative_total(i, tax)
# adjust Discount Amount loss in last tax iteration
if (
i == (len(self.doc.get("taxes")) - 1)
and self.discount_amount_applied
and self.doc.discount_amount
and self.doc.apply_discount_on == "Grand Total"
):
self.grand_total_diff = flt(
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"),
)
if not discount_amount_applied:
self.grand_total_for_distributing_discount = doc.taxes[-1].total
else:
self.grand_total_diff = flt(
self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total,
doc.precision("grand_total"),
)
for i, tax in enumerate(doc.taxes):
self.round_off_totals(tax)
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax, ["total"])
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
# if just for valuation, do not add the tax amount in total
@@ -571,16 +582,20 @@ class calculate_taxes_and_totals:
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.grand_total_diff = diff
else:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
else:
self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
self.doc.grand_total - self.doc.net_total - grand_total_diff,
self.doc.precision("total_taxes_and_charges"),
)
else:
@@ -695,6 +710,9 @@ class calculate_taxes_and_totals:
adjusted_net_amount = item.net_amount - distributed_amount
expected_net_total += adjusted_net_amount
item.net_amount = flt(adjusted_net_amount, item.precision("net_amount"))
item.distributed_discount_amount = flt(
distributed_amount, item.precision("distributed_discount_amount")
)
net_total += item.net_amount
# discount amount rounding adjustment
@@ -704,6 +722,10 @@ class calculate_taxes_and_totals:
item.net_amount = flt(
item.net_amount + rounding_difference, item.precision("net_amount")
)
item.distributed_discount_amount = flt(
distributed_amount + rounding_difference,
item.precision("distributed_discount_amount"),
)
net_total += rounding_difference
item.net_rate = (
@@ -718,7 +740,8 @@ class calculate_taxes_and_totals:
self.doc.base_discount_amount = 0
def get_total_for_discount_amount(self):
if self.doc.apply_discount_on == "Net Total":
doc = self.doc
if doc.apply_discount_on == "Net Total" or not doc.get("taxes"):
return self.doc.net_total
total_actual_tax = 0
@@ -738,7 +761,7 @@ class calculate_taxes_and_totals:
"cumulative_tax_amount": total_actual_tax,
}
for tax in self.doc.get("taxes"):
for tax in doc.taxes:
if tax.charge_type in ["Actual", "On Item Quantity"]:
update_actual_tax_dict(tax, tax.tax_amount)
continue
@@ -757,7 +780,7 @@ class calculate_taxes_and_totals:
)
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
return self.doc.grand_total - total_actual_tax
return getattr(self, "grand_total_for_distributing_discount", doc.grand_total) - total_actual_tax
def calculate_total_advance(self):
if not self.doc.docstatus.is_cancelled():

View File

@@ -0,0 +1,61 @@
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase):
def test_distributed_discount_amount(self):
so = make_sales_order(do_not_save=1)
so.apply_discount_on = "Net Total"
so.discount_amount = 100
so.items[0].qty = 5
so.items[0].rate = 100
so.append("items", so.items[0].as_dict())
so.items[1].qty = 5
so.items[1].rate = 200
so.save()
calculate_taxes_and_totals(so)
self.assertAlmostEqual(so.items[0].distributed_discount_amount, 33.33, places=2)
self.assertAlmostEqual(so.items[1].distributed_discount_amount, 66.67, places=2)
self.assertAlmostEqual(so.items[0].net_amount, 466.67, places=2)
self.assertAlmostEqual(so.items[1].net_amount, 933.33, places=2)
self.assertEqual(so.total, 1500)
self.assertEqual(so.net_total, 1400)
self.assertEqual(so.grand_total, 1400)
def test_distributed_discount_amount_with_taxes(self):
so = make_sales_order(do_not_save=1)
so.apply_discount_on = "Grand Total"
so.discount_amount = 100
so.items[0].qty = 5
so.items[0].rate = 100
so.append("items", so.items[0].as_dict())
so.items[1].qty = 5
so.items[1].rate = 200
so.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"included_in_print_rate": True,
"rate": 10,
},
)
so.save()
calculate_taxes_and_totals(so)
# like in test_distributed_discount_amount, but reduced by the included tax
self.assertAlmostEqual(so.items[0].distributed_discount_amount, 33.33 / 1.1, places=2)
self.assertAlmostEqual(so.items[1].distributed_discount_amount, 66.67 / 1.1, places=2)
self.assertAlmostEqual(so.items[0].net_amount, 466.67 / 1.1, places=2)
self.assertAlmostEqual(so.items[1].net_amount, 933.33 / 1.1, places=2)
self.assertEqual(so.total, 1500)
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
self.assertEqual(so.grand_total, 1400)

View File

@@ -2,6 +2,9 @@ import unittest
from functools import partial
import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.core.doctype.user_permission.user_permission import add_user_permissions
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.controllers import queries
@@ -81,3 +84,54 @@ class TestQueries(unittest.TestCase):
def test_default_uoms(self):
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
def test_employee_query_with_user_permissions(self):
# party field is a dynamic link field in Payment Entry doctype with ignore_user_permissions=0
ps = make_property_setter(
doctype="Payment Entry",
fieldname="party",
property="ignore_user_permissions",
value=1,
property_type="Check",
)
ps.save()
user = create_user("test_employee_query@example.com", ("Accounts User", "HR User"))
add_user_permissions(
{
"user": user.name,
"doctype": "Employee",
"docname": "_T-Employee-00001",
"is_default": 1,
"apply_to_all_doctypes": 1,
"applicable_doctypes": [],
"hide_descendants": 0,
}
)
frappe.reload_doc("accounts", "doctype", "payment entry")
frappe.set_user(user.name)
params = {
"doctype": "Employee",
"txt": "",
"searchfield": "name",
"start": 0,
"page_len": 20,
"filters": None,
"reference_doctype": "Payment Entry",
"ignore_user_permissions": 1,
}
result = queries.employee_query(**params)
self.assertGreater(len(result), 1)
ps.delete(ignore_permissions=1, force=1, delete_permanently=1)
frappe.reload_doc("accounts", "doctype", "payment entry")
frappe.clear_cache()
# only one employee should be returned even though ignore_user_permissions is passed as 1
result = queries.employee_query(**params)
self.assertEqual(len(result), 1)
frappe.set_user("Administrator")

View File

@@ -4,6 +4,7 @@
import copy
import json
from collections import defaultdict
import frappe
from frappe import _, msgprint
@@ -913,8 +914,7 @@ class ProductionPlan(Document):
if material_request_list:
material_request_list = [
f"""<a href="/app/Form/Material Request/{m.name}">{m.name}</a>"""
for m in material_request_list
get_link_to_form("Material Request", m.name) for m in material_request_list
]
msgprint(_("{0} created").format(comma_and(material_request_list)))
else:
@@ -925,6 +925,7 @@ class ProductionPlan(Document):
"Fetch sub assembly items and optionally combine them."
self.sub_assembly_items = []
sub_assembly_items_store = [] # temporary store to process all subassembly items
bin_details = frappe._dict()
for row in self.po_items:
if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse:
@@ -939,6 +940,8 @@ class ProductionPlan(Document):
bom_data = []
get_sub_assembly_items(
[item.production_item for item in sub_assembly_items_store],
bin_details,
row.bom_no,
bom_data,
row.planned_qty,
@@ -1528,10 +1531,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details = frappe._dict()
sub_assembly_items = {}
sub_assembly_items = defaultdict(int)
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
for d in doc.get("sub_assembly_items"):
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("qty")
for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
@@ -1560,6 +1563,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
item_details = {}
if doc.get("sub_assembly_items"):
item_details = get_raw_materials_of_sub_assembly_items(
so_item_details[doc.get("sales_order")].keys() if so_item_details else [],
item_details,
company,
bom_no,
@@ -1737,6 +1741,8 @@ def get_item_data(item_code):
def get_sub_assembly_items(
sub_assembly_items,
bin_details,
bom_no,
bom_data,
to_produce_qty,
@@ -1751,25 +1757,27 @@ def get_sub_assembly_items(
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
bin_details = frappe._dict()
if skip_available_sub_assembly_item:
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items:
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
for _bin_dict in bin_details:
for _bin_dict in bin_details[d.item_code]:
if _bin_dict.projected_qty > 0:
if _bin_dict.projected_qty > stock_qty:
if _bin_dict.projected_qty >= stock_qty:
_bin_dict.projected_qty -= stock_qty
stock_qty = 0
continue
else:
stock_qty = stock_qty - _bin_dict.projected_qty
elif warehouse:
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
if stock_qty > 0:
bom_data.append(
frappe._dict(
{
"actual_qty": bin_details[0].get("actual_qty", 0) if bin_details else 0,
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
if bin_details.get(d.item_code)
else 0,
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
@@ -1787,6 +1795,8 @@ def get_sub_assembly_items(
if d.value:
get_sub_assembly_items(
sub_assembly_items,
bin_details,
d.value,
bom_data,
stock_qty,
@@ -1866,7 +1876,13 @@ def get_non_completed_production_plans():
def get_raw_materials_of_sub_assembly_items(
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
existing_sub_assembly_items,
item_details,
company,
bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=1,
):
bei = frappe.qb.DocType("BOM Item")
bom = frappe.qb.DocType("BOM")
@@ -1910,12 +1926,13 @@ def get_raw_materials_of_sub_assembly_items(
for item in items:
key = (item.item_code, item.bom_no)
if item.bom_no and key not in sub_assembly_items:
if (item.bom_no and key not in sub_assembly_items) or (item.item_code in existing_sub_assembly_items):
continue
if item.bom_no:
planned_qty = flt(sub_assembly_items[key])
get_raw_materials_of_sub_assembly_items(
existing_sub_assembly_items,
item_details,
company,
item.bom_no,

View File

@@ -1635,6 +1635,64 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(row.production_item, sf_item)
self.assertEqual(row.qty, 5.0)
def test_calculation_of_sub_assembly_items(self):
make_item("Sub Assembly Item ", properties={"is_stock_item": 1})
make_item("RM Item 1", properties={"is_stock_item": 1})
make_item("RM Item 2", properties={"is_stock_item": 1})
make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"])
make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"])
make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"])
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
make_stock_entry(
item_code="Sub Assembly Item",
qty=80,
purpose="Material Receipt",
to_warehouse="_Test Warehouse - _TC",
)
make_stock_entry(
item_code="RM Item 1", qty=90, purpose="Material Receipt", to_warehouse="_Test Warehouse - _TC"
)
plan = create_production_plan(
skip_available_sub_assembly_item=1,
sub_assembly_warehouse="_Test Warehouse - _TC",
warehouse="_Test Warehouse - _TC",
item_code="_Test FG Item",
skip_getting_mr_items=1,
planned_qty=100,
do_not_save=1,
)
plan.get_items_from = ""
plan.append(
"po_items",
{
"use_multi_level_bom": 1,
"item_code": "_Test FG Item 2",
"bom_no": frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"),
"planned_qty": 50,
"planned_start_date": now_datetime(),
"stock_uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
},
)
plan.save()
plan.get_sub_assembly_items()
self.assertEqual(plan.sub_assembly_items[0].qty, 20)
self.assertEqual(plan.sub_assembly_items[1].qty, 50)
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
)
mr_items = get_items_for_material_requests(plan.as_dict())
self.assertEqual(mr_items[0].get("quantity"), 80)
self.assertEqual(mr_items[1].get("quantity"), 70)
def create_production_plan(**args):
"""

View File

@@ -342,12 +342,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
var me = this;
this.grand_total_diff = 0;
var actual_tax_dict = {};
// maintain actual tax rate based on idx
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
$.each(doc.taxes, function(i, tax) {
if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
}
@@ -355,7 +357,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
$.each(this.frm._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
$.each(doc.taxes, function(i, tax) {
// tax_amount represents the amount of tax for the current step
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
if (frappe.flags.round_row_wise_tax) {
@@ -400,29 +402,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
tax.grand_total_for_current_item =
flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount);
}
// set precision in the last item iteration
if (n == me.frm._items.length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each item
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax, ["total"]);
// adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
me.grand_total_diff = flt(me.frm.doc.grand_total -
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
}
}
});
});
const discount_amount_applied = this.discount_amount_applied;
if (doc.apply_discount_on === "Grand Total" && (discount_amount_applied || doc.discount_amount || doc.additional_discount_percentage)) {
const tax_amount_precision = precision("tax_amount", doc.taxes[0]);
for (const [i, tax] of doc.taxes.entries()) {
if (discount_amount_applied)
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax_amount_precision);
this.set_cumulative_total(i, tax);
}
if (!this.discount_amount_applied) {
this.grand_total_for_distributing_discount = doc.taxes[doc.taxes.length - 1].total;
} else {
this.grand_total_diff = flt(
this.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[doc.taxes.length - 1].total, precision("grand_total"));
}
}
for (const [i, tax] of doc.taxes.entries()) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each tax
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax, ["total"]);
}
}
set_cumulative_total(row_idx, tax) {
@@ -571,10 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_totals() {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff
: this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
@@ -606,7 +621,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
- this.grand_total_diff, precision("total_taxes_and_charges"));
- grand_total_diff, precision("total_taxes_and_charges"));
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
@@ -729,8 +744,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
get_total_for_discount_amount() {
if(this.frm.doc.apply_discount_on == "Net Total")
return this.frm.doc.net_total;
const doc = this.frm.doc;
if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length)
return doc.net_total;
let total_actual_tax = 0.0;
let actual_taxes_dict = {};
@@ -745,7 +762,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
};
}
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
doc.taxes.forEach(tax => {
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
update_actual_taxes_dict(tax, tax.tax_amount);
return;
@@ -760,7 +777,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
});
return this.frm.doc.grand_total - total_actual_tax;
return (this.grand_total_for_distributing_discount || doc.grand_total) - total_actual_tax;
}
calculate_total_advance(update_paid_amount) {

View File

@@ -240,13 +240,16 @@ erpnext.setup.fiscal_years = {
Afghanistan: ["12-21", "12-20"],
Australia: ["07-01", "06-30"],
Bangladesh: ["07-01", "06-30"],
Canada: ["04-01", "03-31"],
"Costa Rica": ["10-01", "09-30"],
Egypt: ["07-01", "06-30"],
Ethiopia: ["07-08", "07-07"],
"Hong Kong": ["04-01", "03-31"],
India: ["04-01", "03-31"],
Iran: ["06-23", "06-22"],
Kenya: ["07-01", "06-30"],
Malaysia: ["07-01", "06-30"],
Myanmar: ["04-01", "03-31"],
Nepal: ["07-16", "07-15"],
"New Zealand": ["04-01", "03-31"],
Pakistan: ["07-01", "06-30"],
Singapore: ["04-01", "03-31"],

View File

@@ -77,35 +77,34 @@ erpnext.accounts.dimensions = {
},
update_dimension(frm, doctype) {
if (this.accounting_dimensions) {
this.accounting_dimensions.forEach((dimension) => {
if (frm.is_new()) {
if (
frm.doc.company &&
Object.keys(this.default_dimensions || {}).length > 0 &&
this.default_dimensions[frm.doc.company]
) {
let default_dimension =
this.default_dimensions[frm.doc.company][dimension["fieldname"]];
if (
!this.accounting_dimensions ||
!frm.is_new() ||
!frm.doc.company ||
!this.default_dimensions?.[frm.doc.company]
)
return;
if (default_dimension) {
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
frm.set_value(dimension["fieldname"], default_dimension);
}
$.each(frm.doc.items || frm.doc.accounts || [], function (i, row) {
frappe.model.set_value(
row.doctype,
row.name,
dimension["fieldname"],
default_dimension
);
});
}
}
}
});
// don't set default dimensions if any of the dimension is already set due to mapping
if (frm.doc.__onload?.load_after_mapping) {
for (const dimension of this.accounting_dimensions) {
if (frm.doc[dimension["fieldname"]]) return;
}
}
this.accounting_dimensions.forEach((dimension) => {
const default_dimension = this.default_dimensions[frm.doc.company][dimension["fieldname"]];
if (!default_dimension) return;
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
frm.set_value(dimension["fieldname"], default_dimension);
}
(frm.doc.items || frm.doc.accounts || []).forEach((row) => {
frappe.model.set_value(row.doctype, row.name, dimension["fieldname"], default_dimension);
});
});
},
copy_dimension_from_first_row(frm, cdt, cdn, fieldname) {

View File

@@ -111,6 +111,33 @@ erpnext.sales_common = {
this.toggle_editable_price_list_rate();
}
company() {
super.company();
this.set_default_company_address();
}
set_default_company_address() {
if (!frappe.meta.has_field(this.frm.doc.doctype, "company_address")) return;
var me = this;
if (this.frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: {
name: this.frm.doc.company,
existing_address: this.frm.doc.company_address || "",
},
debounce: 2000,
callback: function (r) {
if (r.message) {
me.frm.set_value("company_address", r.message);
} else {
me.frm.set_value("company_address", "");
}
},
});
}
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function () {

View File

@@ -38,6 +38,7 @@
"column_break_18",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"section_break1",
"rate",
@@ -238,7 +239,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -668,6 +669,12 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
@@ -691,7 +698,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-12-12 13:49:17.765883",
"modified": "2024-12-12 13:49:18.765883",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",

View File

@@ -33,6 +33,7 @@ class QuotationItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
gross_profit: DF.Currency
has_alternative_item: DF.Check
image: DF.Attach | None

View File

@@ -164,27 +164,6 @@ frappe.ui.form.on("Sales Order", {
);
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: {
name: frm.doc.company,
existing_address: frm.doc.company_address || "",
},
debounce: 2000,
callback: function (r) {
if (r.message) {
frm.set_value("company_address", r.message);
} else {
frm.set_value("company_address", "");
}
},
});
}
},
onload: function (frm) {
if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today());

View File

@@ -40,6 +40,7 @@
"column_break_19",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"section_break_simple1",
"rate",
@@ -287,7 +288,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -913,6 +914,12 @@
"print_hide": 1,
"report_hide": 1
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
@@ -964,7 +971,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-02-28 09:45:43.934947",
"modified": "2025-02-28 09:45:44.934947",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -40,6 +40,7 @@ class SalesOrderItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
ensure_delivery_based_on_produced_serial_no: DF.Check
grant_commission: DF.Check
gross_profit: DF.Currency

View File

@@ -188,6 +188,7 @@ erpnext.PointOfSale.ItemCart = class {
await me.events.checkout();
me.toggle_checkout_btn(false);
me.disable_customer_selection();
me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
});
@@ -195,6 +196,7 @@ erpnext.PointOfSale.ItemCart = class {
this.$totals_section.on("click", ".edit-cart-btn", () => {
this.events.edit_cart();
this.toggle_checkout_btn(true);
me.enable_customer_selection();
});
this.$component.on("click", ".add-discount-wrapper", () => {
@@ -698,6 +700,25 @@ erpnext.PointOfSale.ItemCart = class {
}
}
disable_customer_selection() {
this.$customer_section.find(".reset-customer-btn").css("visibility", "hidden");
this.$customer_section.off("click", ".customer-display");
this.$customer_section.off("click", ".reset-customer-btn");
}
enable_customer_selection() {
this.$customer_section.find(".reset-customer-btn").css("visibility", "visible");
this.$customer_section.on("click", ".customer-display", (e) => {
if ($(e.target).closest(".reset-customer-btn").length) return;
const show = this.$cart_container.is(":visible");
this.toggle_customer_info(show);
});
this.$customer_section.on("click", ".reset-customer-btn", () => {
this.reset_customer_selector();
});
}
highlight_checkout_btn(toggle) {
if (toggle) {
this.$add_discount_elem.css("display", "flex");

View File

@@ -282,6 +282,7 @@ class Company(NestedSet):
frappe.clear_cache()
def create_default_warehouses(self):
parent_warehouse = None
for wh_detail in [
{"warehouse_name": _("All Warehouses"), "is_group": 1},
{"warehouse_name": _("Stores"), "is_group": 0},
@@ -289,24 +290,31 @@ class Company(NestedSet):
{"warehouse_name": _("Finished Goods"), "is_group": 0},
{"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"},
]:
if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)):
warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": wh_detail["warehouse_name"],
"is_group": wh_detail["is_group"],
"company": self.name,
"parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr)
if not wh_detail["is_group"]
else "",
"warehouse_type": wh_detail["warehouse_type"]
if "warehouse_type" in wh_detail
else None,
}
)
warehouse.flags.ignore_permissions = True
warehouse.flags.ignore_mandatory = True
warehouse.insert()
if frappe.db.exists(
"Warehouse",
{
"warehouse_name": wh_detail["warehouse_name"],
"company": self.name,
},
):
continue
warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": wh_detail["warehouse_name"],
"is_group": wh_detail["is_group"],
"company": self.name,
"parent_warehouse": parent_warehouse,
"warehouse_type": wh_detail.get("warehouse_type"),
}
)
warehouse.flags.ignore_permissions = True
warehouse.flags.ignore_mandatory = True
warehouse.insert()
if wh_detail["is_group"]:
parent_warehouse = warehouse.name
def create_default_accounts(self):
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts

View File

@@ -85,20 +85,21 @@ class Employee(NestedSet):
self.reset_employee_emails_cache()
def update_user_permissions(self):
if not self.create_user_permission:
return
if not has_permission("User Permission", ptype="write", raise_exception=False):
if not has_permission("User Permission", ptype="write") or (
not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission")
):
return
employee_user_permission_exists = frappe.db.exists(
"User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id}
)
if employee_user_permission_exists:
return
add_user_permission("Employee", self.name, self.user_id)
add_user_permission("Company", self.company, self.user_id)
if employee_user_permission_exists and not self.create_user_permission:
remove_user_permission("Employee", self.name, self.user_id)
remove_user_permission("Company", self.company, self.user_id)
elif not employee_user_permission_exists and self.create_user_permission:
add_user_permission("Employee", self.name, self.user_id)
add_user_permission("Company", self.company, self.user_id)
def update_user(self):
# add employee role if missing

View File

@@ -2,5 +2,20 @@
// For license information, please see license.txt
frappe.ui.form.on("Bin", {
refresh: function (frm) {},
refresh(frm) {
frm.trigger("recalculate_bin_quantity");
},
recalculate_bin_quantity(frm) {
frm.add_custom_button(__("Recalculate Bin Qty"), () => {
frappe.call({
method: "recalculate_qty",
freeze: true,
doc: frm.doc,
callback: function (r) {
frappe.show_alert(__("Bin Qty Recalculated"), 2);
},
});
});
},
});

View File

@@ -35,6 +35,28 @@ class Bin(Document):
warehouse: DF.Link
# end: auto-generated types
@frappe.whitelist()
def recalculate_qty(self):
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
from erpnext.stock.stock_balance import (
get_indented_qty,
get_ordered_qty,
get_planned_qty,
get_reserved_qty,
)
self.actual_qty = get_actual_qty(self.item_code, self.warehouse)
self.planned_qty = get_planned_qty(self.item_code, self.warehouse)
self.indented_qty = get_indented_qty(self.item_code, self.warehouse)
self.ordered_qty = get_ordered_qty(self.item_code, self.warehouse)
self.reserved_qty = get_reserved_qty(self.item_code, self.warehouse)
self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse)
self.update_reserved_qty_for_sub_contracting(update_qty=False)
self.update_reserved_qty_for_production_plan(skip_project_qty_update=True, update_qty=False)
self.set_projected_qty()
self.save()
def before_save(self):
if self.get("__islocal") or not self.stock_uom:
self.stock_uom = frappe.get_cached_value("Item", self.item_code, "stock_uom")
@@ -52,7 +74,7 @@ class Bin(Document):
- flt(self.reserved_qty_for_production_plan)
)
def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False):
def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False, update_qty=True):
"""Update qty reserved for production from Production Plan tables
in open production plan"""
from erpnext.manufacturing.doctype.production_plan.production_plan import (
@@ -68,11 +90,12 @@ class Bin(Document):
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
update_modified=True,
)
if update_qty:
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
update_modified=True,
)
if not skip_project_qty_update:
self.set_projected_qty()
@@ -115,7 +138,9 @@ class Bin(Document):
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"):
def update_reserved_qty_for_sub_contracting(
self, subcontract_doctype="Subcontracting Order", update_qty=True
):
# reserved qty
subcontract_order = frappe.qb.DocType(subcontract_doctype)
@@ -191,9 +216,11 @@ class Bin(Document):
else:
reserved_qty_for_sub_contract = 0
self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True)
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
self.reserved_qty_for_sub_contract = reserved_qty_for_sub_contract
if update_qty:
self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True)
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_stock(self):
"""Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry"""
@@ -235,27 +262,10 @@ def update_qty(bin_name, args):
bin_details = get_bin_details(bin_name)
# actual qty is already updated by processing current voucher
actual_qty = bin_details.actual_qty or 0.0
sle = frappe.qb.DocType("Stock Ledger Entry")
# actual qty is not up to date in case of backdated transaction
if future_sle_exists(args, allow_force_reposting=False):
last_sle_qty = (
frappe.qb.from_(sle)
.select(sle.qty_after_transaction)
.where(
(sle.item_code == args.get("item_code"))
& (sle.warehouse == args.get("warehouse"))
& (sle.is_cancelled == 0)
)
.orderby(sle.posting_datetime, order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.limit(1)
.run()
)
actual_qty = 0.0
if last_sle_qty:
actual_qty = last_sle_qty[0][0]
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
@@ -287,3 +297,23 @@ def update_qty(bin_name, args):
},
update_modified=True,
)
def get_actual_qty(item_code, warehouse):
sle = frappe.qb.DocType("Stock Ledger Entry")
last_sle_qty = (
frappe.qb.from_(sle)
.select(sle.qty_after_transaction)
.where((sle.item_code == item_code) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(sle.posting_datetime, order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.limit(1)
.run()
)
actual_qty = 0.0
if last_sle_qty:
actual_qty = last_sle_qty[0][0]
return actual_qty

View File

@@ -2521,6 +2521,28 @@ class TestDeliveryNote(FrappeTestCase):
for d in bundle_data:
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no])
def test_delivery_note_per_billed_after_return(self):
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
so = make_sales_order(qty=2)
dn = make_delivery_note(so.name)
dn.submit()
self.assertEqual(dn.per_billed, 0)
si = make_sales_invoice(dn.name)
si.location = "Test Location"
si.submit()
dn_return = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True)
dn_return.items[0].dn_detail = dn.items[0].name
dn_return.submit()
returned = frappe.get_doc("Delivery Note", dn_return.name)
returned.update_prevdoc_status()
dn.load_from_db()
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.per_returned, 100)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -40,6 +40,7 @@
"column_break_19",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"section_break_1",
"rate",
@@ -277,7 +278,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -912,6 +913,12 @@
"fieldname": "column_break_rxvc",
"fieldtype": "Column Break"
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
@@ -934,7 +941,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-02-05 14:28:32.322181",
"modified": "2025-02-05 14:28:33.322181",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -37,6 +37,7 @@ class DeliveryNoteItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Float
distributed_discount_amount: DF.Currency
dn_detail: DF.Data | None
expense_account: DF.Link | None
grant_commission: DF.Check

View File

@@ -107,14 +107,6 @@ frappe.ui.form.on("Material Request", {
if (flt(frm.doc.per_received, precision) < 100) {
frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped"));
if (frm.doc.material_request_type === "Purchase") {
frm.add_custom_button(
__("Purchase Order"),
() => frm.events.make_purchase_order(frm),
__("Create")
);
}
}
if (flt(frm.doc.per_ordered, precision) < 100) {
@@ -158,14 +150,18 @@ frappe.ui.form.on("Material Request", {
}
if (frm.doc.material_request_type === "Purchase") {
frm.add_custom_button(
__("Purchase Order"),
() => frm.events.make_purchase_order(frm),
__("Create")
);
frm.add_custom_button(
__("Request for Quotation"),
() => frm.events.make_request_for_quotation(frm),
__("Create")
);
}
if (frm.doc.material_request_type === "Purchase") {
frm.add_custom_button(
__("Supplier Quotation"),
() => frm.events.make_supplier_quotation(frm),
@@ -181,6 +177,14 @@ frappe.ui.form.on("Material Request", {
);
}
if (frm.doc.material_request_type === "Subcontracting") {
frm.add_custom_button(
__("Subcontracted Purchase Order"),
() => frm.events.make_purchase_order(frm),
__("Create")
);
}
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
}

View File

@@ -357,7 +357,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2023-09-15 12:07:24.789471",
"modified": "2025-04-21 18:36:04.827917",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@@ -379,7 +379,7 @@ def set_missing_values(source, target_doc):
def update_item(obj, target, source_parent):
target.conversion_factor = obj.conversion_factor
qty = obj.received_qty or obj.ordered_qty
qty = obj.ordered_qty or obj.received_qty
target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor
target.stock_qty = target.qty * target.conversion_factor
if getdate(target.schedule_date) < getdate(nowdate()):
@@ -432,7 +432,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
qty = d.received_qty or d.ordered_qty
qty = d.ordered_qty or d.received_qty
return qty < d.stock_qty and child_filter
@@ -721,6 +721,7 @@ def make_stock_entry(source_name, target_doc=None):
"uom": "stock_uom",
"job_card_item": "job_card_item",
},
"field_no_map": ["expense_account"],
"postprocess": update_item,
"condition": lambda doc: (
flt(doc.ordered_qty, doc.precision("ordered_qty"))

View File

@@ -17,7 +17,7 @@ frappe.listview_settings["Purchase Receipt"] = {
return [__("Closed"), "green", "status,=,Closed"];
} else if (flt(doc.per_returned, 2) === 100) {
return [__("Return Issued"), "grey", "per_returned,=,100|docstatus,=,1"];
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) {
} else if (flt(doc.grand_total || doc.base_grand_total) !== 0 && flt(doc.per_billed, 2) == 0) {
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];

View File

@@ -48,6 +48,7 @@
"column_break_37",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"base_rate_with_margin",
"sec_break1",
"rate",
@@ -911,7 +912,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin_section",
"fieldtype": "Section Break",
"label": "Discount and Margin"
@@ -1128,6 +1129,12 @@
"options": "Company:company:default_currency",
"print_hide": 1
},
{
"fieldname": "distributed_discount_amount",
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"fieldname": "amount_difference_with_purchase_invoice",
"fieldtype": "Currency",
@@ -1140,7 +1147,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-12 17:10:42.780622",
"modified": "2025-03-12 17:10:43.780622",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -37,6 +37,7 @@ class PurchaseReceiptItem(Document):
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
expense_account: DF.Link | None
from_warehouse: DF.Link | None
has_item_scanned: DF.Check