mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-13 10:11:20 +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'
|
exclude: 'node_modules|.git'
|
||||||
default_stages: [commit]
|
default_stages: [pre-commit]
|
||||||
fail_fast: false
|
fail_fast: false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
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
|
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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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"];
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user