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' exclude: 'node_modules|.git'
default_stages: [commit] default_stages: [pre-commit]
fail_fast: false fail_fast: false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -196,7 +196,7 @@
"fieldname": "item_wise_tax_detail", "fieldname": "item_wise_tax_detail",
"fieldtype": "Code", "fieldtype": "Code",
"hidden": 1, "hidden": 1,
"label": "Item Wise Tax Detail ", "label": "Item Wise Tax Detail",
"oldfieldname": "item_wise_tax_detail", "oldfieldname": "item_wise_tax_detail",
"oldfieldtype": "Small Text", "oldfieldtype": "Small Text",
"print_hide": 1, "print_hide": 1,
@@ -235,10 +235,11 @@
"read_only": 1 "read_only": 1
} }
], ],
"grid_page_length": 50,
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-04-08 19:51:36.678551", "modified": "2025-04-15 13:14:48.936047",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Taxes and Charges", "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) { onload: function (frm) {
frm.redemption_conversion_factor = null; frm.redemption_conversion_factor = null;
}, },

View File

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

View File

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

View File

@@ -100,7 +100,7 @@ class PartyLedgerSummaryReport:
conditions.append(doctype.territory.isin(self.filters.territory)) conditions.append(doctype.territory.isin(self.filters.territory))
if self.filters.get(group_field): 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: if self.filters.payment_terms_template:
conditions.append(doctype.payment_terms == 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: for field in expected:
with self.subTest(field=field): with self.subTest(field=field):
self.assertEqual(report_output[0].get(field), expected.get(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 import frappe
from frappe import _ from frappe import _
from frappe.utils import getdate
def execute(filters=None): def execute(filters=None):
@@ -33,6 +34,7 @@ def execute(filters=None):
def validate_filters(filters): def validate_filters(filters):
"""Validate if dates are properly set""" """Validate if dates are properly set"""
filters = frappe._dict(filters or {})
if filters.from_date > filters.to_date: if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before 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: if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("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 net_total_map.get((voucher_type, name)):
if voucher_type == "Journal Entry" and tax_amount and rate: if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount # 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): def get_tax_rate_map(filters):
rate_map = frappe.get_all( rate_map = frappe.get_all(
"Tax Withholding Rate", "Tax Withholding Rate",
filters={ filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
"from_date": ("<=", filters.get("from_date")), fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
"to_date": (">=", filters.get("to_date")),
},
fields=["parent", "tax_withholding_rate"],
as_list=1,
) )
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 import frappe
from frappe.tests.utils import FrappeTestCase 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.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
@@ -60,6 +60,56 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
] ]
self.check_expected_values(result, expected_values) 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): def check_expected_values(self, result, expected_values):
for i in range(len(result)): for i in range(len(result)):
voucher = frappe._dict(result[i]) voucher = frappe._dict(result[i])

View File

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

View File

@@ -37,6 +37,7 @@ class PurchaseOrderItem(Document):
description: DF.TextEditor | None description: DF.TextEditor | None
discount_amount: DF.Currency discount_amount: DF.Currency
discount_percentage: DF.Percent discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
expected_delivery_date: DF.Date | None expected_delivery_date: DF.Date | None
expense_account: DF.Link | None expense_account: DF.Link | None
fg_item: 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.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) { make_supplier_quotation: function (frm) {
var doc = frm.doc; var doc = frm.doc;
var dialog = new frappe.ui.Dialog({ var dialog = new frappe.ui.Dialog({

View File

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

View File

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

View File

@@ -1841,8 +1841,11 @@ class AccountsController(TransactionBase):
and self.get("discount_amount") and self.get("discount_amount")
and self.get("additional_discount_account") and self.get("additional_discount_account")
): ):
amount = item.amount amount += item.distributed_discount_amount
base_amount = item.base_amount base_amount += flt(
item.distributed_discount_amount * self.get("conversion_rate"),
item.precision("distributed_discount_amount"),
)
return amount, base_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") base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
) )
d.outstanding = d.payment_amount d.outstanding = d.payment_amount
d.base_outstanding = flt( d.base_outstanding = d.base_payment_amount
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
)
elif not d.invoice_portion: elif not d.invoice_portion:
d.base_payment_amount = flt( d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount") d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
) )
d.base_outstanding = d.base_payment_amount
else: else:
self.fetch_payment_terms_from_order( self.fetch_payment_terms_from_order(
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms 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 import frappe
from frappe import qb, scrub from frappe import qb, scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond 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 import Criterion, CustomFunction
from frappe.query_builder.functions import Concat, Locate, Sum 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 from pypika import Order
import erpnext import erpnext
@@ -20,10 +21,28 @@ from erpnext.stock.get_item_details import _get_item_tax_template
# searches for active employees # searches for active employees
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @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" doctype = "Employee"
conditions = [] conditions = []
fields = get_fields(doctype, ["name", "employee_name"]) 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( return frappe.db.sql(
"""select {fields} from `tabEmployee` """select {fields} from `tabEmployee`
@@ -42,13 +61,32 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
"fields": ", ".join(fields), "fields": ", ".join(fields),
"key": searchfield, "key": searchfield,
"fcond": get_filters_cond(doctype, filters, conditions), "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}, {"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 # searches for leads which are not converted
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @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( return frappe.get_all(
"UOM", "UOM",
filters={"name": ["like", f"%{txt}%"]}, filters={"name": ["like", f"%{txt}%"], "enabled": 1},
fields=["name"], fields=["name"],
limit_start=start, limit_start=start,
limit_page_length=page_len, limit_page_length=page_len,

View File

@@ -84,8 +84,8 @@ status_map = {
"Delivery Note": [ "Delivery Note": [
["Draft", None], ["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], ["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"], ["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"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and 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): def update_billing_percentage(self, update_modified=True):
target_ref_field = "amount" target_ref_field = "amount"
if self.doctype == "Delivery Note": 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( self._update_percent_field(
{ {
@@ -1153,6 +1159,12 @@ class StockController(AccountsController):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return 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() item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
if not item_wise_transfer_qty: if not item_wise_transfer_qty:
return return
@@ -1182,15 +1194,11 @@ class StockController(AccountsController):
bold(key[1]), bold(key[1]),
bold(flt(transferred_qty, precision)), bold(flt(transferred_qty, precision)),
bold(parent_doctype), 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): 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 = { parent_doctype = {
"Purchase Receipt": "Delivery Note", "Purchase Receipt": "Delivery Note",
"Purchase Invoice": "Sales Invoice", "Purchase Invoice": "Sales Invoice",
@@ -1210,7 +1218,7 @@ class StockController(AccountsController):
child_tab.item_code, child_tab.item_code,
child_tab.qty, 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) data = query.run(as_dict=True)

View File

@@ -377,20 +377,22 @@ class calculate_taxes_and_totals:
self._calculate() self._calculate()
def calculate_taxes(self): 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 # maintain actual tax rate based on idx
actual_tax_dict = dict( actual_tax_dict = dict(
[ [
[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] [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" if tax.charge_type == "Actual"
] ]
) )
for n, item in enumerate(self._items): for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate) 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 # tax_amount represents the amount of tax for the current step
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map) current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
if frappe.flags.round_row_wise_tax: 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) tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount)
else: else:
tax.grand_total_for_current_item = flt( 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 discount_amount_applied = self.discount_amount_applied
if n == len(self._items) - 1: if doc.apply_discount_on == "Grand Total" and (
self.round_off_totals(tax) discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) ):
tax_amount_precision = doc.taxes[0].precision("tax_amount")
self.round_off_base_values(tax) for i, tax in enumerate(doc.taxes):
self.set_cumulative_total(i, tax) 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 not discount_amount_applied:
if ( self.grand_total_for_distributing_discount = doc.taxes[-1].total
i == (len(self.doc.get("taxes")) - 1) else:
and self.discount_amount_applied self.grand_total_diff = flt(
and self.doc.discount_amount self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total,
and self.doc.apply_discount_on == "Grand Total" doc.precision("grand_total"),
): )
self.grand_total_diff = flt(
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, for i, tax in enumerate(doc.taxes):
self.doc.precision("rounding_adjustment"), 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): 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 # 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")): if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.grand_total_diff = diff self.grand_total_diff = diff
else:
self.grand_total_diff = 0
def calculate_totals(self): def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
if self.doc.get("taxes"): 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: else:
self.doc.grand_total = flt(self.doc.net_total) self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"): if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt( 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"), self.doc.precision("total_taxes_and_charges"),
) )
else: else:
@@ -695,6 +710,9 @@ class calculate_taxes_and_totals:
adjusted_net_amount = item.net_amount - distributed_amount adjusted_net_amount = item.net_amount - distributed_amount
expected_net_total += adjusted_net_amount expected_net_total += adjusted_net_amount
item.net_amount = flt(adjusted_net_amount, item.precision("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 net_total += item.net_amount
# discount amount rounding adjustment # discount amount rounding adjustment
@@ -704,6 +722,10 @@ class calculate_taxes_and_totals:
item.net_amount = flt( item.net_amount = flt(
item.net_amount + rounding_difference, item.precision("net_amount") 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 net_total += rounding_difference
item.net_rate = ( item.net_rate = (
@@ -718,7 +740,8 @@ class calculate_taxes_and_totals:
self.doc.base_discount_amount = 0 self.doc.base_discount_amount = 0
def get_total_for_discount_amount(self): 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 return self.doc.net_total
total_actual_tax = 0 total_actual_tax = 0
@@ -738,7 +761,7 @@ class calculate_taxes_and_totals:
"cumulative_tax_amount": total_actual_tax, "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"]: if tax.charge_type in ["Actual", "On Item Quantity"]:
update_actual_tax_dict(tax, tax.tax_amount) update_actual_tax_dict(tax, tax.tax_amount)
continue continue
@@ -757,7 +780,7 @@ class calculate_taxes_and_totals:
) )
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100) 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): def calculate_total_advance(self):
if not self.doc.docstatus.is_cancelled(): 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 from functools import partial
import frappe 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 from erpnext.controllers import queries
@@ -81,3 +84,54 @@ class TestQueries(unittest.TestCase):
def test_default_uoms(self): def test_default_uoms(self):
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10) 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 copy
import json import json
from collections import defaultdict
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
@@ -913,8 +914,7 @@ class ProductionPlan(Document):
if material_request_list: if material_request_list:
material_request_list = [ material_request_list = [
f"""<a href="/app/Form/Material Request/{m.name}">{m.name}</a>""" get_link_to_form("Material Request", m.name) for m in material_request_list
for m in material_request_list
] ]
msgprint(_("{0} created").format(comma_and(material_request_list))) msgprint(_("{0} created").format(comma_and(material_request_list)))
else: else:
@@ -925,6 +925,7 @@ class ProductionPlan(Document):
"Fetch sub assembly items and optionally combine them." "Fetch sub assembly items and optionally combine them."
self.sub_assembly_items = [] self.sub_assembly_items = []
sub_assembly_items_store = [] # temporary store to process all subassembly items sub_assembly_items_store = [] # temporary store to process all subassembly items
bin_details = frappe._dict()
for row in self.po_items: for row in self.po_items:
if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse: if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse:
@@ -939,6 +940,8 @@ class ProductionPlan(Document):
bom_data = [] bom_data = []
get_sub_assembly_items( get_sub_assembly_items(
[item.production_item for item in sub_assembly_items_store],
bin_details,
row.bom_no, row.bom_no,
bom_data, bom_data,
row.planned_qty, 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() 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"): if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
for d in 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: for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_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 = {} item_details = {}
if doc.get("sub_assembly_items"): if doc.get("sub_assembly_items"):
item_details = get_raw_materials_of_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, item_details,
company, company,
bom_no, bom_no,
@@ -1737,6 +1741,8 @@ def get_item_data(item_code):
def get_sub_assembly_items( def get_sub_assembly_items(
sub_assembly_items,
bin_details,
bom_no, bom_no,
bom_data, bom_data,
to_produce_qty, to_produce_qty,
@@ -1751,25 +1757,27 @@ def get_sub_assembly_items(
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
bin_details = frappe._dict() if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items:
if skip_available_sub_assembly_item: bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
bin_details = 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 > 0:
if _bin_dict.projected_qty > stock_qty: if _bin_dict.projected_qty >= stock_qty:
_bin_dict.projected_qty -= stock_qty
stock_qty = 0 stock_qty = 0
continue continue
else: else:
stock_qty = stock_qty - _bin_dict.projected_qty stock_qty = stock_qty - _bin_dict.projected_qty
elif warehouse: 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: if stock_qty > 0:
bom_data.append( bom_data.append(
frappe._dict( 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, "parent_item_code": parent_item_code,
"description": d.description, "description": d.description,
"production_item": d.item_code, "production_item": d.item_code,
@@ -1787,6 +1795,8 @@ def get_sub_assembly_items(
if d.value: if d.value:
get_sub_assembly_items( get_sub_assembly_items(
sub_assembly_items,
bin_details,
d.value, d.value,
bom_data, bom_data,
stock_qty, stock_qty,
@@ -1866,7 +1876,13 @@ def get_non_completed_production_plans():
def get_raw_materials_of_sub_assembly_items( 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") bei = frappe.qb.DocType("BOM Item")
bom = frappe.qb.DocType("BOM") bom = frappe.qb.DocType("BOM")
@@ -1910,12 +1926,13 @@ def get_raw_materials_of_sub_assembly_items(
for item in items: for item in items:
key = (item.item_code, item.bom_no) 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 continue
if item.bom_no: if item.bom_no:
planned_qty = flt(sub_assembly_items[key]) planned_qty = flt(sub_assembly_items[key])
get_raw_materials_of_sub_assembly_items( get_raw_materials_of_sub_assembly_items(
existing_sub_assembly_items,
item_details, item_details,
company, company,
item.bom_no, item.bom_no,

View File

@@ -1635,6 +1635,64 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(row.production_item, sf_item) self.assertEqual(row.production_item, sf_item)
self.assertEqual(row.qty, 5.0) 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): def create_production_plan(**args):
""" """

View File

@@ -342,12 +342,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
calculate_taxes() { calculate_taxes() {
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
var me = this; var me = this;
this.grand_total_diff = 0;
var actual_tax_dict = {}; var actual_tax_dict = {};
// maintain actual tax rate based on idx // 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") { if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax)); 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) { $.each(this.frm._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); 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 // 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); var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
if (frappe.flags.round_row_wise_tax) { 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 = tax.grand_total_for_current_item =
flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount); 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) { set_cumulative_total(row_idx, tax) {
@@ -571,10 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_totals() { calculate_totals() {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency // Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
var me = this; const me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; 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.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); : this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { 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.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"]); 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() { get_total_for_discount_amount() {
if(this.frm.doc.apply_discount_on == "Net Total") const doc = this.frm.doc;
return this.frm.doc.net_total;
if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length)
return doc.net_total;
let total_actual_tax = 0.0; let total_actual_tax = 0.0;
let actual_taxes_dict = {}; 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)) { if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
update_actual_taxes_dict(tax, tax.tax_amount); update_actual_taxes_dict(tax, tax.tax_amount);
return; 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); 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) { calculate_total_advance(update_paid_amount) {

View File

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

View File

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

View File

@@ -111,6 +111,33 @@ erpnext.sales_common = {
this.toggle_editable_price_list_rate(); 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() { customer() {
var me = this; var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function () { erpnext.utils.get_party_details(this.frm, null, null, function () {

View File

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

View File

@@ -33,6 +33,7 @@ class QuotationItem(Document):
description: DF.TextEditor | None description: DF.TextEditor | None
discount_amount: DF.Currency discount_amount: DF.Currency
discount_percentage: DF.Percent discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
gross_profit: DF.Currency gross_profit: DF.Currency
has_alternative_item: DF.Check has_alternative_item: DF.Check
image: DF.Attach | None 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) { onload: function (frm) {
if (!frm.doc.transaction_date) { if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today()); frm.set_value("transaction_date", frappe.datetime.get_today());

View File

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

View File

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

View File

@@ -188,6 +188,7 @@ erpnext.PointOfSale.ItemCart = class {
await me.events.checkout(); await me.events.checkout();
me.toggle_checkout_btn(false); me.toggle_checkout_btn(false);
me.disable_customer_selection();
me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); 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.$totals_section.on("click", ".edit-cart-btn", () => {
this.events.edit_cart(); this.events.edit_cart();
this.toggle_checkout_btn(true); this.toggle_checkout_btn(true);
me.enable_customer_selection();
}); });
this.$component.on("click", ".add-discount-wrapper", () => { 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) { highlight_checkout_btn(toggle) {
if (toggle) { if (toggle) {
this.$add_discount_elem.css("display", "flex"); this.$add_discount_elem.css("display", "flex");

View File

@@ -282,6 +282,7 @@ class Company(NestedSet):
frappe.clear_cache() frappe.clear_cache()
def create_default_warehouses(self): def create_default_warehouses(self):
parent_warehouse = None
for wh_detail in [ for wh_detail in [
{"warehouse_name": _("All Warehouses"), "is_group": 1}, {"warehouse_name": _("All Warehouses"), "is_group": 1},
{"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Stores"), "is_group": 0},
@@ -289,24 +290,31 @@ class Company(NestedSet):
{"warehouse_name": _("Finished Goods"), "is_group": 0}, {"warehouse_name": _("Finished Goods"), "is_group": 0},
{"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}, {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"},
]: ]:
if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)): if frappe.db.exists(
warehouse = frappe.get_doc( "Warehouse",
{ {
"doctype": "Warehouse", "warehouse_name": wh_detail["warehouse_name"],
"warehouse_name": wh_detail["warehouse_name"], "company": self.name,
"is_group": wh_detail["is_group"], },
"company": self.name, ):
"parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr) continue
if not wh_detail["is_group"]
else "", warehouse = frappe.get_doc(
"warehouse_type": wh_detail["warehouse_type"] {
if "warehouse_type" in wh_detail "doctype": "Warehouse",
else None, "warehouse_name": wh_detail["warehouse_name"],
} "is_group": wh_detail["is_group"],
) "company": self.name,
warehouse.flags.ignore_permissions = True "parent_warehouse": parent_warehouse,
warehouse.flags.ignore_mandatory = True "warehouse_type": wh_detail.get("warehouse_type"),
warehouse.insert() }
)
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): def create_default_accounts(self):
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts 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() self.reset_employee_emails_cache()
def update_user_permissions(self): def update_user_permissions(self):
if not self.create_user_permission: if not has_permission("User Permission", ptype="write") or (
return not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission")
if not has_permission("User Permission", ptype="write", raise_exception=False): ):
return return
employee_user_permission_exists = frappe.db.exists( employee_user_permission_exists = frappe.db.exists(
"User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id} "User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id}
) )
if employee_user_permission_exists: if employee_user_permission_exists and not self.create_user_permission:
return remove_user_permission("Employee", self.name, self.user_id)
remove_user_permission("Company", self.company, self.user_id)
add_user_permission("Employee", self.name, self.user_id) elif not employee_user_permission_exists and self.create_user_permission:
add_user_permission("Company", self.company, self.user_id) add_user_permission("Employee", self.name, self.user_id)
add_user_permission("Company", self.company, self.user_id)
def update_user(self): def update_user(self):
# add employee role if missing # add employee role if missing

View File

@@ -2,5 +2,20 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Bin", { 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 warehouse: DF.Link
# end: auto-generated types # 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): def before_save(self):
if self.get("__islocal") or not self.stock_uom: if self.get("__islocal") or not self.stock_uom:
self.stock_uom = frappe.get_cached_value("Item", self.item_code, "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) - 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 """Update qty reserved for production from Production Plan tables
in open production plan""" in open production plan"""
from erpnext.manufacturing.doctype.production_plan.production_plan import ( 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.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
self.db_set( if update_qty:
"reserved_qty_for_production_plan", self.db_set(
flt(self.reserved_qty_for_production_plan), "reserved_qty_for_production_plan",
update_modified=True, flt(self.reserved_qty_for_production_plan),
) update_modified=True,
)
if not skip_project_qty_update: if not skip_project_qty_update:
self.set_projected_qty() self.set_projected_qty()
@@ -115,7 +138,9 @@ class Bin(Document):
self.set_projected_qty() self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True) 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 # reserved qty
subcontract_order = frappe.qb.DocType(subcontract_doctype) subcontract_order = frappe.qb.DocType(subcontract_doctype)
@@ -191,9 +216,11 @@ class Bin(Document):
else: else:
reserved_qty_for_sub_contract = 0 reserved_qty_for_sub_contract = 0
self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) self.reserved_qty_for_sub_contract = reserved_qty_for_sub_contract
self.set_projected_qty() if update_qty:
self.db_set("projected_qty", self.projected_qty, update_modified=True) 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): def update_reserved_stock(self):
"""Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry""" """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) bin_details = get_bin_details(bin_name)
# actual qty is already updated by processing current voucher # actual qty is already updated by processing current voucher
actual_qty = bin_details.actual_qty or 0.0 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 # actual qty is not up to date in case of backdated transaction
if future_sle_exists(args, allow_force_reposting=False): if future_sle_exists(args, allow_force_reposting=False):
last_sle_qty = ( actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
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]
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_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, 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: for d in bundle_data:
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) 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): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

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

View File

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

View File

@@ -107,14 +107,6 @@ frappe.ui.form.on("Material Request", {
if (flt(frm.doc.per_received, precision) < 100) { if (flt(frm.doc.per_received, precision) < 100) {
frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped")); 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) { 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") { if (frm.doc.material_request_type === "Purchase") {
frm.add_custom_button(
__("Purchase Order"),
() => frm.events.make_purchase_order(frm),
__("Create")
);
frm.add_custom_button( frm.add_custom_button(
__("Request for Quotation"), __("Request for Quotation"),
() => frm.events.make_request_for_quotation(frm), () => frm.events.make_request_for_quotation(frm),
__("Create") __("Create")
); );
}
if (frm.doc.material_request_type === "Purchase") {
frm.add_custom_button( frm.add_custom_button(
__("Supplier Quotation"), __("Supplier Quotation"),
() => frm.events.make_supplier_quotation(frm), () => 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")); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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