mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-21 18:36:30 +00:00
Merge pull request #47204 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
61
erpnext/controllers/tests/test_distributed_discount.py
Normal file
61
erpnext/controllers/tests/test_distributed_discount.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user