mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-25 01:28:29 +00:00
Merge pull request #53142 from frappe/version-15-hotfix
This commit is contained in:
@@ -33,6 +33,12 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
def on_update(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
if self.is_short_year:
|
||||
|
||||
@@ -1092,20 +1092,32 @@ class PaymentEntry(AccountsController):
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
flt(
|
||||
(
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
),
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
/ self.source_exchange_rate
|
||||
)
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
flt(
|
||||
(
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
),
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
/ self.target_exchange_rate
|
||||
)
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
|
||||
@@ -346,8 +346,7 @@ def apply_pricing_rule(args, doc=None):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not args.transaction_type:
|
||||
set_transaction_type(args)
|
||||
set_transaction_type(args)
|
||||
|
||||
# list of dictionaries
|
||||
out = []
|
||||
@@ -683,23 +682,23 @@ def remove_pricing_rules(item_list):
|
||||
return out
|
||||
|
||||
|
||||
def set_transaction_type(args):
|
||||
if args.transaction_type:
|
||||
def set_transaction_type(pricing_ctx: frappe._dict) -> None:
|
||||
if pricing_ctx.transaction_type in ["buying", "selling"]:
|
||||
return
|
||||
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
args.transaction_type = "selling"
|
||||
elif args.doctype in (
|
||||
if pricing_ctx.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
pricing_ctx.transaction_type = "selling"
|
||||
elif pricing_ctx.doctype in (
|
||||
"Material Request",
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
):
|
||||
args.transaction_type = "buying"
|
||||
elif args.customer:
|
||||
args.transaction_type = "selling"
|
||||
pricing_ctx.transaction_type = "buying"
|
||||
elif pricing_ctx.customer:
|
||||
pricing_ctx.transaction_type = "selling"
|
||||
else:
|
||||
args.transaction_type = "buying"
|
||||
pricing_ctx.transaction_type = "buying"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
@@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
|
||||
def test_payment_terms_template_filters(self):
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
|
||||
payment_term1 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
|
||||
).insert()
|
||||
payment_term2 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
|
||||
).insert()
|
||||
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50",
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term1.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 15,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term2.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 30,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"based_on_payment_terms": 1,
|
||||
"payment_terms_template": template.name,
|
||||
"ageing_based_on": "Posting Date",
|
||||
}
|
||||
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.payment_terms_template = template.name
|
||||
schedule = get_payment_terms(template.name)
|
||||
pi.set("payment_schedule", [])
|
||||
|
||||
for row in schedule:
|
||||
row["due_date"] = add_days(pi.posting_date, row.get("credit_days", 0))
|
||||
pi.append("payment_schedule", row)
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
@@ -1029,9 +1029,8 @@ class ReceivablePayableReport:
|
||||
self,
|
||||
):
|
||||
self.customer = qb.DocType("Customer")
|
||||
|
||||
if self.filters.get("customer_group"):
|
||||
groups = get_customer_group_with_children(self.filters.customer_group)
|
||||
groups = get_party_group_with_children("Customer", self.filters.customer_group)
|
||||
customers = (
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
@@ -1043,14 +1042,18 @@ class ReceivablePayableReport:
|
||||
self.get_hierarchical_filters("Territory", "territory")
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
customer_ptt = self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
|
||||
si_ptt = self.add_payment_term_template_filters("Sales Invoice")
|
||||
|
||||
sales_ptt = self.ple.against_voucher_no.isin(si_ptt)
|
||||
|
||||
self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt]))
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
@@ -1075,14 +1078,53 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.payment_terms == self.filters.get("supplier_group"))
|
||||
)
|
||||
supplier_ptt = self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
|
||||
pi_ptt = self.add_payment_term_template_filters("Purchase Invoice")
|
||||
|
||||
purchase_ptt = self.ple.against_voucher_no.isin(pi_ptt)
|
||||
|
||||
self.qb_selection_filter.append(Criterion.any([supplier_ptt, purchase_ptt]))
|
||||
|
||||
def add_payment_term_template_filters(self, dtype):
|
||||
voucher_type = qb.DocType(dtype)
|
||||
|
||||
ptt = (
|
||||
qb.from_(voucher_type)
|
||||
.select(voucher_type.name)
|
||||
.where(voucher_type.payment_terms_template == self.filters.get("payment_terms_template"))
|
||||
.where(voucher_type.company == self.filters.company)
|
||||
)
|
||||
|
||||
if dtype == "Purchase Invoice":
|
||||
party = "Supplier"
|
||||
party_group_type = "supplier_group"
|
||||
acc_type = "credit_to"
|
||||
else:
|
||||
party = "Customer"
|
||||
party_group_type = "customer_group"
|
||||
acc_type = "debit_to"
|
||||
|
||||
if self.filters.get(party_group_type):
|
||||
party_groups = get_party_group_with_children(party, self.filters.get(party_group_type))
|
||||
ptt = ptt.where((voucher_type[party_group_type]).isin(party_groups))
|
||||
|
||||
if self.filters.party:
|
||||
ptt = ptt.where((voucher_type[party.lower()]).isin(self.filters.party))
|
||||
|
||||
if self.filters.cost_center:
|
||||
cost_centers = get_cost_centers_with_children(self.filters.cost_center)
|
||||
ptt = ptt.where(voucher_type.cost_center.isin(cost_centers))
|
||||
|
||||
if self.filters.party_account:
|
||||
ptt = ptt.where(voucher_type[acc_type] == self.filters.party_account)
|
||||
|
||||
return ptt
|
||||
|
||||
def get_hierarchical_filters(self, doctype, key):
|
||||
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
||||
|
||||
@@ -1320,20 +1362,26 @@ class ReceivablePayableReport:
|
||||
self.err_journals = [x[0] for x in results] if results else []
|
||||
|
||||
|
||||
def get_customer_group_with_children(customer_groups):
|
||||
if not isinstance(customer_groups, list):
|
||||
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
|
||||
def get_party_group_with_children(party, party_groups):
|
||||
if party not in ("Customer", "Supplier"):
|
||||
return []
|
||||
|
||||
all_customer_groups = []
|
||||
for d in customer_groups:
|
||||
if frappe.db.exists("Customer Group", d):
|
||||
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_customer_groups += [c.name for c in children]
|
||||
group_dtype = f"{party} Group"
|
||||
if not isinstance(party_groups, list):
|
||||
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
|
||||
|
||||
all_party_groups = []
|
||||
for d in party_groups:
|
||||
if frappe.db.exists(group_dtype, d):
|
||||
lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name"
|
||||
)
|
||||
all_party_groups += children
|
||||
else:
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
return list(set(all_party_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
|
||||
@@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
def test_payment_terms_template_filters(self):
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
|
||||
payment_term1 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
|
||||
).insert()
|
||||
payment_term2 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
|
||||
).insert()
|
||||
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50",
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term1.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 15,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term2.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 30,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"based_on_payment_terms": 1,
|
||||
"payment_terms_template": template.name,
|
||||
"ageing_based_on": "Posting Date",
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.payment_terms_template = template.name
|
||||
schedule = get_payment_terms(template.name)
|
||||
si.set("payment_schedule", [])
|
||||
|
||||
for row in schedule:
|
||||
row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0))
|
||||
si.append("payment_schedule", row)
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
@@ -96,7 +96,7 @@ def execute(filters=None):
|
||||
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
|
||||
chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
@@ -225,18 +225,19 @@ def get_report_summary(
|
||||
], (net_asset - net_liability + net_equity)
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
|
||||
labels = [col.get("label") for col in chart_columns]
|
||||
|
||||
asset_data, liability_data, equity_data = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for col in chart_columns:
|
||||
key = col.get("key") or col.get("fieldname")
|
||||
if asset:
|
||||
asset_data.append(asset[-2].get(p.get("fieldname")))
|
||||
asset_data.append(asset[-2].get(key))
|
||||
if liability:
|
||||
liability_data.append(liability[-2].get(p.get("fieldname")))
|
||||
liability_data.append(liability[-2].get(key))
|
||||
if equity:
|
||||
equity_data.append(equity[-2].get(p.get("fieldname")))
|
||||
equity_data.append(equity[-2].get(key))
|
||||
|
||||
datasets = []
|
||||
if asset_data:
|
||||
|
||||
@@ -139,7 +139,7 @@ def execute(filters=None):
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
chart = get_chart_data(period_list, data, company_currency)
|
||||
|
||||
report_summary = get_report_summary(summary_data, company_currency)
|
||||
|
||||
@@ -411,13 +411,12 @@ def get_report_summary(summary_data, currency):
|
||||
return report_summary
|
||||
|
||||
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
print(data)
|
||||
def get_chart_data(period_list, data, currency):
|
||||
labels = [period.get("label") for period in period_list]
|
||||
datasets = [
|
||||
{
|
||||
"name": section.get("section").replace("'", ""),
|
||||
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
|
||||
"values": [section.get(period.get("key")) for period in period_list],
|
||||
}
|
||||
for section in data
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
|
||||
@@ -48,22 +48,25 @@ def execute(filters=None):
|
||||
return columns, data, message, chart
|
||||
|
||||
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
|
||||
companies_column, companies = get_companies(filters)
|
||||
columns = get_columns(companies_column, filters)
|
||||
company_list, companies = get_companies(filters)
|
||||
company_columns = get_company_columns(company_list, filters)
|
||||
columns = get_columns(company_columns)
|
||||
|
||||
if filters.get("report") == "Balance Sheet":
|
||||
data, message, chart, report_summary = get_balance_sheet_data(
|
||||
fiscal_year, companies, columns, filters
|
||||
fiscal_year, companies, company_columns, filters
|
||||
)
|
||||
elif filters.get("report") == "Profit and Loss Statement":
|
||||
data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters)
|
||||
data, message, chart, report_summary = get_profit_loss_data(
|
||||
fiscal_year, companies, company_columns, filters
|
||||
)
|
||||
else:
|
||||
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
|
||||
|
||||
return columns, data, message, chart, report_summary
|
||||
|
||||
|
||||
def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
|
||||
asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
|
||||
|
||||
liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters)
|
||||
@@ -116,7 +119,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
|
||||
chart = get_chart_data(filters, company_columns, asset, liability, equity, company_currency)
|
||||
|
||||
return data, message, chart, report_summary
|
||||
|
||||
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
|
||||
return root_account[0][0]
|
||||
|
||||
|
||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
|
||||
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
|
||||
company_currency = get_company_currency(filters)
|
||||
|
||||
@@ -174,7 +177,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
if net_profit_loss:
|
||||
data.append(net_profit_loss)
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
|
||||
chart = get_pl_chart_data(filters, company_columns, income, expense, net_profit_loss, company_currency)
|
||||
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
@@ -279,7 +282,30 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_columns(companies, filters):
|
||||
def get_company_columns(companies, filters):
|
||||
company_columns = []
|
||||
for company in companies:
|
||||
apply_currency_formatter = 1 if not filters.presentation_currency else 0
|
||||
currency = filters.presentation_currency
|
||||
if not currency:
|
||||
currency = erpnext.get_company_currency(company)
|
||||
|
||||
company_columns.append(
|
||||
{
|
||||
"fieldname": company,
|
||||
"label": f"{company} ({currency})",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
"apply_currency_formatter": apply_currency_formatter,
|
||||
"company_name": company,
|
||||
}
|
||||
)
|
||||
|
||||
return company_columns
|
||||
|
||||
|
||||
def get_columns(company_columns):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
@@ -297,23 +323,7 @@ def get_columns(companies, filters):
|
||||
},
|
||||
]
|
||||
|
||||
for company in companies:
|
||||
apply_currency_formatter = 1 if not filters.presentation_currency else 0
|
||||
currency = filters.presentation_currency
|
||||
if not currency:
|
||||
currency = erpnext.get_company_currency(company)
|
||||
|
||||
columns.append(
|
||||
{
|
||||
"fieldname": company,
|
||||
"label": f"{company} ({currency})",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
"apply_currency_formatter": apply_currency_formatter,
|
||||
"company_name": company,
|
||||
}
|
||||
)
|
||||
columns.extend(company_columns)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def execute(filters=None):
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
|
||||
chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
@@ -158,18 +158,20 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
||||
return net_profit_loss
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
|
||||
labels = [col.get("label") for col in chart_columns]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for col in chart_columns:
|
||||
key = col.get("key") or col.get("fieldname")
|
||||
|
||||
if income:
|
||||
income_data.append(income[-2].get(p.get("fieldname")))
|
||||
income_data.append(income[-2].get(key))
|
||||
if expense:
|
||||
expense_data.append(expense[-2].get(p.get("fieldname")))
|
||||
expense_data.append(expense[-2].get(key))
|
||||
if net_profit_loss:
|
||||
net_profit.append(net_profit_loss.get(p.get("fieldname")))
|
||||
net_profit.append(net_profit_loss.get(key))
|
||||
|
||||
datasets = []
|
||||
if income_data:
|
||||
|
||||
@@ -345,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
prepare_opening_closing(d)
|
||||
|
||||
|
||||
def calculate_total_row(accounts, company_currency):
|
||||
def calculate_total_row(data, company_currency, show_group_accounts=True):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
@@ -362,10 +362,16 @@ def calculate_total_row(accounts, company_currency):
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
if not d.parent_account:
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
def sum_value_fields(row):
|
||||
for field in value_fields:
|
||||
total_row[field] += row[field]
|
||||
|
||||
for d in data:
|
||||
if not show_group_accounts:
|
||||
sum_value_fields(d)
|
||||
|
||||
elif show_group_accounts and not d.get("parent_account"):
|
||||
sum_value_fields(d)
|
||||
|
||||
return total_row
|
||||
|
||||
@@ -409,11 +415,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
total_row = calculate_total_row(
|
||||
data, company_currency, show_group_accounts=filters.get("show_group_accounts")
|
||||
)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
|
||||
@@ -250,10 +250,17 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
"subject",
|
||||
])
|
||||
.then((r) => {
|
||||
frm.set_value(
|
||||
"message_for_supplier",
|
||||
r.message.use_html ? r.message.response_html : r.message.response
|
||||
);
|
||||
if (r.message.use_html) {
|
||||
frm.set_value({
|
||||
mfs_html: r.message.response_html,
|
||||
use_html: 1,
|
||||
});
|
||||
} else {
|
||||
frm.set_value({
|
||||
message_for_supplier: r.message.response,
|
||||
use_html: 0,
|
||||
});
|
||||
}
|
||||
frm.set_value("subject", r.message.subject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
"send_document_print",
|
||||
"sec_break_email_2",
|
||||
"subject",
|
||||
"use_html",
|
||||
"message_for_supplier",
|
||||
"mfs_html",
|
||||
"terms_section_break",
|
||||
"incoterm",
|
||||
"named_place",
|
||||
@@ -142,12 +144,13 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Please supply the specified items at the best possible rates",
|
||||
"depends_on": "eval:doc.use_html == 0",
|
||||
"fieldname": "message_for_supplier",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Message for Supplier",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:doc.use_html == 0",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -324,6 +327,22 @@
|
||||
"label": "Subject",
|
||||
"not_nullable": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:doc.use_html == 1",
|
||||
"fieldname": "mfs_html",
|
||||
"fieldtype": "Code",
|
||||
"label": "Message for Supplier",
|
||||
"mandatory_depends_on": "eval:doc.use_html == 1",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_html",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Use HTML"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -331,7 +350,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-05 14:27:33.329810",
|
||||
"modified": "2026-03-01 23:38:48.079274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -47,7 +47,8 @@ class RequestforQuotation(BuyingController):
|
||||
incoterm: DF.Link | None
|
||||
items: DF.Table[RequestforQuotationItem]
|
||||
letter_head: DF.Link | None
|
||||
message_for_supplier: DF.TextEditor
|
||||
message_for_supplier: DF.TextEditor | None
|
||||
mfs_html: DF.Code | None
|
||||
named_place: DF.Data | None
|
||||
naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
|
||||
opportunity: DF.Link | None
|
||||
@@ -61,6 +62,7 @@ class RequestforQuotation(BuyingController):
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
transaction_date: DF.Date
|
||||
use_html: DF.Check
|
||||
vendor: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -100,8 +102,16 @@ class RequestforQuotation(BuyingController):
|
||||
["use_html", "response", "response_html", "subject"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not self.message_for_supplier:
|
||||
self.message_for_supplier = data.response_html if data.use_html else data.response
|
||||
|
||||
self.use_html = data.use_html
|
||||
|
||||
if data.use_html:
|
||||
if not self.mfs_html:
|
||||
self.mfs_html = data.response_html
|
||||
else:
|
||||
if not self.message_for_supplier:
|
||||
self.message_for_supplier = data.response
|
||||
|
||||
if not self.subject:
|
||||
self.subject = data.subject
|
||||
|
||||
@@ -304,7 +314,10 @@ class RequestforQuotation(BuyingController):
|
||||
else:
|
||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||
|
||||
rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
|
||||
message_template = self.mfs_html if self.use_html else self.message_for_supplier
|
||||
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
|
||||
rendered_message = frappe.render_template(message_template, doc_args)
|
||||
|
||||
subject_source = (
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
|
||||
@@ -165,7 +165,7 @@ def get_data(filters):
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"requestor": mr_record.get("owner", po.owner),
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
|
||||
@@ -936,7 +936,14 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
|
||||
|
||||
if doctype == "Packed Item":
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
key = frappe.get_cached_value(
|
||||
"Packed Item",
|
||||
{"parent_detail_docname": row.voucher_detail_no, "item_code": row.item_code},
|
||||
field,
|
||||
)
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
|
||||
if row.voucher_type == "Delivery Note":
|
||||
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
||||
elif row.voucher_type == "Sales Invoice":
|
||||
|
||||
@@ -318,9 +318,10 @@ class SellingController(StockController):
|
||||
if is_internal_customer or not is_stock_item:
|
||||
continue
|
||||
|
||||
if item.get("incoming_rate") and item.base_net_rate < (
|
||||
rate_field = "valuation_rate" if self.doctype in ["Sales Order", "Quotation"] else "incoming_rate"
|
||||
if item.get(rate_field) and item.base_net_rate < (
|
||||
valuation_rate := flt(
|
||||
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
item.get(rate_field) * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
)
|
||||
):
|
||||
throw_message(
|
||||
|
||||
@@ -57,6 +57,8 @@ class StockController(AccountsController):
|
||||
|
||||
if not self.get("is_return"):
|
||||
self.validate_inspection()
|
||||
|
||||
self.validate_warehouse_of_sabb()
|
||||
self.validate_serialized_batch()
|
||||
self.clean_serial_nos()
|
||||
self.validate_customer_provided_item()
|
||||
@@ -65,6 +67,45 @@ class StockController(AccountsController):
|
||||
self.validate_putaway_capacity()
|
||||
self.reset_conversion_factor()
|
||||
|
||||
def validate_warehouse_of_sabb(self):
|
||||
if self.is_internal_transfer():
|
||||
return
|
||||
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
for row in self.items:
|
||||
if not row.get("serial_and_batch_bundle"):
|
||||
continue
|
||||
|
||||
sabb_details = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
row.serial_and_batch_bundle,
|
||||
["type_of_transaction", "warehouse", "has_serial_no"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not sabb_details:
|
||||
continue
|
||||
|
||||
if sabb_details.type_of_transaction != "Outward":
|
||||
continue
|
||||
|
||||
warehouse = row.get("warehouse") or row.get("s_warehouse")
|
||||
if sabb_details.warehouse != warehouse:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}."
|
||||
).format(row.idx, warehouse, sabb_details.warehouse, row.serial_and_batch_bundle)
|
||||
)
|
||||
|
||||
if self.doctype == "Stock Reconciliation":
|
||||
continue
|
||||
|
||||
if sabb_details.has_serial_no and doc_before_save and doc_before_save.get("items"):
|
||||
prev_row = doc_before_save.get("items", {"idx": row.idx})
|
||||
if prev_row and prev_row[0].serial_and_batch_bundle != row.serial_and_batch_bundle:
|
||||
sabb_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
sabb_doc.validate_serial_no_status()
|
||||
|
||||
def reset_conversion_factor(self):
|
||||
for row in self.get("items"):
|
||||
if row.uom != row.stock_uom:
|
||||
@@ -1661,7 +1702,7 @@ def check_item_quality_inspection(doctype, items):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
def make_quality_inspections(company, doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
@@ -1680,6 +1721,7 @@ def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
quality_inspection = frappe.get_doc(
|
||||
{
|
||||
"company": company,
|
||||
"doctype": "Quality Inspection",
|
||||
"inspection_type": "Incoming",
|
||||
"inspected_by": frappe.session.user,
|
||||
|
||||
@@ -307,6 +307,21 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("uom", "items", function (doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
|
||||
if (!row.item_code) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_item_uom_query",
|
||||
filters: {
|
||||
item_code: row.item_code,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
me.frm.set_query("contact_person", erpnext.queries["contact_query"]);
|
||||
|
||||
if (me.frm.doc.opportunity_from == "Lead") {
|
||||
|
||||
@@ -59,7 +59,9 @@ def create_prospect_against_crm_deal():
|
||||
)
|
||||
pass
|
||||
|
||||
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
||||
if doc.contacts and len(doc.contacts):
|
||||
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
||||
|
||||
create_address("Prospect", prospect_name, doc.address)
|
||||
frappe.response["message"] = prospect_name
|
||||
|
||||
|
||||
@@ -6,24 +6,25 @@ import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges
|
||||
from erpnext.stock.report.stock_analytics.stock_analytics import (
|
||||
get_period,
|
||||
get_period_columns,
|
||||
get_period_date_ranges,
|
||||
)
|
||||
|
||||
WORK_ORDER_STATUS_LIST = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns(filters)
|
||||
data, chart = get_data(filters, columns)
|
||||
period_columns = get_period_columns(filters)
|
||||
columns = get_columns(period_columns)
|
||||
data, chart = get_data(filters, period_columns)
|
||||
return columns, data, None, chart
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
def get_columns(period_columns):
|
||||
columns = [{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 140}]
|
||||
ranges = get_period_date_ranges(filters)
|
||||
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120})
|
||||
columns.extend(period_columns)
|
||||
|
||||
return columns
|
||||
|
||||
@@ -49,7 +50,7 @@ def get_work_orders(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_data(filters, columns):
|
||||
def get_data(filters, period_columns):
|
||||
ranges = build_ranges(filters)
|
||||
period_labels = [scrub(pd) for _fd, _td, pd in ranges]
|
||||
periodic_data = {status: {pd: 0 for pd in period_labels} for status in WORK_ORDER_STATUS_LIST}
|
||||
@@ -84,7 +85,7 @@ def get_data(filters, columns):
|
||||
row[scrub(period)] = periodic_data[status].get(scrub(period), 0)
|
||||
data.append(row)
|
||||
|
||||
chart = get_chart_data(periodic_data, columns)
|
||||
chart = get_chart_data(periodic_data, period_columns)
|
||||
return data, chart
|
||||
|
||||
|
||||
@@ -103,9 +104,9 @@ def build_ranges(filters):
|
||||
return ranges
|
||||
|
||||
|
||||
def get_chart_data(periodic_data, columns):
|
||||
period_labels = [d.get("label") for d in columns[1:]]
|
||||
period_fieldnames = [d.get("fieldname") for d in columns[1:]]
|
||||
def get_chart_data(periodic_data, period_columns):
|
||||
period_labels = [col.get("label") for col in period_columns]
|
||||
period_fieldnames = [col.get("fieldname") for col in period_columns]
|
||||
|
||||
datasets = []
|
||||
for status in WORK_ORDER_STATUS_LIST:
|
||||
|
||||
@@ -2476,6 +2476,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||
args: {
|
||||
company: me.frm.doc.company,
|
||||
doctype: me.frm.doc.doctype,
|
||||
docname: me.frm.doc.name,
|
||||
items: selected_data,
|
||||
|
||||
@@ -2472,6 +2472,49 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
si2 = make_sales_invoice(so.name)
|
||||
self.assertEqual(si2.items[0].qty, 20)
|
||||
|
||||
@change_settings("Selling Settings", {"validate_selling_price": 1})
|
||||
def test_selling_price_validation_for_manufactured_item(self):
|
||||
"""
|
||||
Unit test to check the selling price validation for manufactured item, without last purchae rate in Item master.
|
||||
"""
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
# create a FG Item and RM Item
|
||||
rm_item = make_item(
|
||||
"_Test RM Item for SO selling validation",
|
||||
{"is_stock_item": 1, "valuation_rate": 100, "stock_uom": "Nos"},
|
||||
).name
|
||||
rm_warehouse = create_warehouse("_Test RM SPV Warehouse")
|
||||
fg_item = make_item("_Test FG Item for SO selling validation", {"is_stock_item": 1}).name
|
||||
fg_warehouse = create_warehouse("_Test FG SPV Warehouse")
|
||||
|
||||
# create BOM and inward entry for RM Item
|
||||
bom_no = make_bom(item=fg_item, raw_materials=[rm_item]).name
|
||||
make_stock_entry(item_code=rm_item, target=rm_warehouse, qty=10, rate=100)
|
||||
|
||||
# create a manufacture entry, so system won't update the last purchase rate in Item master.
|
||||
se = make_stock_entry(item_code=fg_item, qty=10, purpose="Manufacture", do_not_save=True)
|
||||
|
||||
se.from_bom = 1
|
||||
se.use_multi_level_bom = 1
|
||||
se.bom_no = bom_no
|
||||
se.fg_completed_qty = 1
|
||||
se.from_warehouse = rm_warehouse
|
||||
se.to_warehouse = fg_warehouse
|
||||
|
||||
se.get_items()
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
# check valuation of FG Item
|
||||
self.assertEqual(se.items[1].valuation_rate, 100)
|
||||
|
||||
# create a SO for FG Item with selling rate than valuation rate.
|
||||
so = make_sales_order(item_code=fg_item, qty=10, rate=50, warehouse=fg_warehouse, do_not_save=1)
|
||||
self.assertRaises(frappe.ValidationError, so.save)
|
||||
|
||||
|
||||
def compare_payment_schedules(doc, doc1, doc2):
|
||||
for index, schedule in enumerate(doc1.get("payment_schedule")):
|
||||
|
||||
@@ -242,7 +242,7 @@ class Item(Document):
|
||||
'Image in the description has been removed. To disable this behavior, uncheck "{0}" in {1}.'
|
||||
).format(
|
||||
frappe.get_meta("Stock Settings").get_label("clean_description_html"),
|
||||
get_link_to_form("Stock Settings"),
|
||||
get_link_to_form("Stock Settings", "Stock Settings"),
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
frappe.listview_settings["Item"] = {
|
||||
add_fields: ["item_name", "stock_uom", "item_group", "image", "has_variants", "end_of_life", "disabled"],
|
||||
add_fields: [
|
||||
"item_name",
|
||||
"stock_uom",
|
||||
"item_group",
|
||||
"image",
|
||||
"has_variants",
|
||||
"end_of_life",
|
||||
"disabled",
|
||||
"variant_of",
|
||||
],
|
||||
filters: [["disabled", "=", "0"]],
|
||||
|
||||
get_indicator: function (doc) {
|
||||
|
||||
@@ -14,6 +14,10 @@ from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class IncorrectCompanyValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class LandedCostVoucher(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -66,6 +70,7 @@ class LandedCostVoucher(Document):
|
||||
self.check_mandatory()
|
||||
self.validate_receipt_documents()
|
||||
self.validate_line_items()
|
||||
self.validate_expense_accounts()
|
||||
init_landed_taxes_and_totals(self)
|
||||
self.set_total_taxes_and_charges()
|
||||
if not self.get("items"):
|
||||
@@ -101,11 +106,28 @@ class LandedCostVoucher(Document):
|
||||
receipt_documents = []
|
||||
|
||||
for d in self.get("purchase_receipts"):
|
||||
docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
|
||||
docstatus, company = frappe.get_cached_value(
|
||||
d.receipt_document_type, d.receipt_document, ["docstatus", "company"]
|
||||
)
|
||||
if docstatus != 1:
|
||||
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
|
||||
frappe.throw(_(msg), title=_("Invalid Document"))
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: {1} {2} is linked to company {3}. Please select a document belonging to company {4}."
|
||||
).format(
|
||||
d.idx,
|
||||
d.receipt_document_type,
|
||||
frappe.bold(d.receipt_document),
|
||||
frappe.bold(company),
|
||||
frappe.bold(self.company),
|
||||
),
|
||||
title=_("Incorrect Company"),
|
||||
exc=IncorrectCompanyValidationError,
|
||||
)
|
||||
|
||||
if d.receipt_document_type == "Purchase Invoice":
|
||||
update_stock = frappe.db.get_value(
|
||||
d.receipt_document_type, d.receipt_document, "update_stock"
|
||||
@@ -137,6 +159,24 @@ class LandedCostVoucher(Document):
|
||||
_("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code)
|
||||
)
|
||||
|
||||
def validate_expense_accounts(self):
|
||||
for t in self.taxes:
|
||||
company = frappe.get_cached_value("Account", t.expense_account, "company")
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}."
|
||||
).format(
|
||||
t.idx,
|
||||
frappe.bold(t.expense_account),
|
||||
frappe.bold(company),
|
||||
frappe.bold(self.company),
|
||||
),
|
||||
title=_("Incorrect Account"),
|
||||
exc=IncorrectCompanyValidationError,
|
||||
)
|
||||
|
||||
def set_total_taxes_and_charges(self):
|
||||
self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes"))
|
||||
|
||||
|
||||
@@ -174,6 +174,39 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
|
||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||
|
||||
def test_lcv_validates_company(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
|
||||
IncorrectCompanyValidationError,
|
||||
)
|
||||
|
||||
company_a = "_Test Company"
|
||||
company_b = "_Test Company with perpetual inventory"
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company=company_a,
|
||||
warehouse="Stores - _TC",
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
lcv = make_landed_cost_voucher(
|
||||
company=company_b,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=50,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertRaises(IncorrectCompanyValidationError, lcv.validate_receipt_documents)
|
||||
lcv.company = company_a
|
||||
|
||||
self.assertRaises(IncorrectCompanyValidationError, lcv.validate_expense_accounts)
|
||||
lcv.taxes[0].expense_account = get_expense_account(company_a)
|
||||
|
||||
lcv.save()
|
||||
distribute_landed_cost_on_items(lcv)
|
||||
lcv.submit()
|
||||
|
||||
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
||||
"Test impact of LCV on future stock balances."
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -1076,6 +1109,7 @@ def make_landed_cost_voucher(**args):
|
||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||
lcv.company = args.company or "_Test Company"
|
||||
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
|
||||
expense_account = get_expense_account(args.company or "_Test Company")
|
||||
|
||||
lcv.set(
|
||||
"purchase_receipts",
|
||||
@@ -1090,16 +1124,17 @@ def make_landed_cost_voucher(**args):
|
||||
],
|
||||
)
|
||||
|
||||
lcv.set(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"description": "Shipping Charges",
|
||||
"expense_account": args.expense_account or "Expenses Included In Valuation - TCP1",
|
||||
"amount": args.charges,
|
||||
}
|
||||
],
|
||||
)
|
||||
if args.charges:
|
||||
lcv.set(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"description": "Shipping Charges",
|
||||
"expense_account": args.expense_account or expense_account,
|
||||
"amount": args.charges,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
if not args.do_not_save:
|
||||
lcv.insert()
|
||||
@@ -1115,6 +1150,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||
lcv.company = company
|
||||
lcv.distribute_charges_based_on = "Amount"
|
||||
expense_account = get_expense_account(company)
|
||||
|
||||
lcv.set(
|
||||
"purchase_receipts",
|
||||
@@ -1134,7 +1170,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
||||
[
|
||||
{
|
||||
"description": "Insurance Charges",
|
||||
"expense_account": "Expenses Included In Valuation - TCP1",
|
||||
"expense_account": expense_account,
|
||||
"amount": charges,
|
||||
}
|
||||
],
|
||||
@@ -1149,6 +1185,11 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
||||
return lcv
|
||||
|
||||
|
||||
def get_expense_account(company):
|
||||
company_abbr = frappe.get_cached_value("Company", company, "abbr")
|
||||
return f"Expenses Included In Valuation - {company_abbr}"
|
||||
|
||||
|
||||
def distribute_landed_cost_on_items(lcv):
|
||||
based_on = lcv.distribute_charges_based_on.lower()
|
||||
total = sum(flt(d.get(based_on)) for d in lcv.get("items"))
|
||||
|
||||
@@ -298,7 +298,8 @@ class MaterialRequest(BuyingController):
|
||||
|
||||
if mr_qty_allowance:
|
||||
allowed_qty = flt(
|
||||
(d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")
|
||||
(d.stock_qty + (d.stock_qty * (mr_qty_allowance / 100))),
|
||||
d.precision("ordered_qty"),
|
||||
)
|
||||
|
||||
if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision):
|
||||
|
||||
@@ -1556,7 +1556,7 @@ def update_common_item_properties(item, location):
|
||||
item.item_code = location.item_code
|
||||
item.s_warehouse = location.warehouse
|
||||
item.transfer_qty = location.picked_qty
|
||||
item.qty = location.qty
|
||||
item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty"))
|
||||
item.uom = location.uom
|
||||
item.conversion_factor = location.conversion_factor
|
||||
item.stock_uom = location.stock_uom
|
||||
|
||||
@@ -1127,6 +1127,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
||||
# Update Billing % based on pending accepted qty
|
||||
buying_settings = frappe.get_single("Buying Settings")
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
|
||||
|
||||
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
|
||||
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
|
||||
@@ -1172,7 +1173,10 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
||||
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
||||
elif amount and item.billed_amt > amount:
|
||||
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
|
||||
if per_over_billed > over_billing_allowance:
|
||||
if (
|
||||
per_over_billed > over_billing_allowance
|
||||
and role_allowed_to_over_bill not in frappe.get_roles()
|
||||
):
|
||||
frappe.throw(
|
||||
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
|
||||
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
|
||||
|
||||
@@ -140,7 +140,8 @@ class TestQualityInspection(FrappeTestCase):
|
||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||
for item in dn.items:
|
||||
item.sample_size = item.qty
|
||||
quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items)
|
||||
|
||||
quality_inspections = make_quality_inspections(dn.company, dn.doctype, dn.name, dn.items)
|
||||
self.assertEqual(len(dn.items), len(quality_inspections))
|
||||
|
||||
# cleanup
|
||||
|
||||
@@ -718,10 +718,13 @@ class SerialandBatchBundle(Document):
|
||||
if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]:
|
||||
rate = frappe.db.get_value(
|
||||
"Packed Item",
|
||||
self.voucher_detail_no,
|
||||
{"parent_detail_docname": self.voucher_detail_no, "item_code": self.item_code},
|
||||
"incoming_rate",
|
||||
)
|
||||
|
||||
if rate is None:
|
||||
rate = frappe.db.get_value("Packed Item", self.voucher_detail_no, "incoming_rate")
|
||||
|
||||
if rate is not None:
|
||||
is_packed_item = True
|
||||
|
||||
@@ -794,6 +797,9 @@ class SerialandBatchBundle(Document):
|
||||
if parent.get("posting_time") and (not self.posting_time or self.posting_time != parent.posting_time):
|
||||
values_to_set["posting_time"] = parent.posting_time
|
||||
|
||||
if row.get("doctype") == "Packed Item" and row.get("parent_detail_docname"):
|
||||
values_to_set["voucher_detail_no"] = row.get("parent_detail_docname")
|
||||
|
||||
if parent.doctype in [
|
||||
"Delivery Note",
|
||||
"Purchase Receipt",
|
||||
@@ -1329,7 +1335,21 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
if not vouchers and self.voucher_type == "Delivery Note":
|
||||
frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
|
||||
if frappe.db.exists("Packed Item", self.voucher_detail_no):
|
||||
frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
|
||||
else:
|
||||
packed_items = frappe.get_all(
|
||||
"Packed Item",
|
||||
filters={
|
||||
"parent_detail_docname": self.voucher_detail_no,
|
||||
"serial_and_batch_bundle": self.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for packed_item in packed_items:
|
||||
frappe.db.set_value("Packed Item", packed_item, "serial_and_batch_bundle", None)
|
||||
|
||||
return
|
||||
|
||||
for voucher in vouchers:
|
||||
|
||||
@@ -207,6 +207,7 @@ class StockEntry(StockController):
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
||||
self.validate_warehouse()
|
||||
self.validate_warehouse_of_sabb()
|
||||
self.validate_work_order()
|
||||
self.validate_bom()
|
||||
self.set_process_loss_qty()
|
||||
|
||||
@@ -2258,6 +2258,53 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "material_consumption", original_value)
|
||||
|
||||
def test_qi_creation_with_naming_rule_company_condition(self):
|
||||
"""
|
||||
Unit test case to check the document naming rule with company condition
|
||||
For Quality Inspection, when created from Stock Entry.
|
||||
"""
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import create_company
|
||||
from erpnext.controllers.stock_controller import make_quality_inspections
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
# create a separate company to handle document naming rule with company condition
|
||||
qc_company = create_company(company_name="Test Quality Company")
|
||||
|
||||
# create document naming rule based on that for Quality Inspection Doctype
|
||||
qc_naming_rule = frappe.new_doc(
|
||||
"Document Naming Rule", document_type="Quality Inspection", prefix="NQC.-ST-", prefix_digits=5
|
||||
)
|
||||
qc_naming_rule.append("conditions", {"field": "company", "condition": "=", "value": qc_company})
|
||||
qc_naming_rule.save()
|
||||
|
||||
warehouse = create_warehouse(warehouse_name="Test QI Warehouse", company=qc_company)
|
||||
item = create_item(
|
||||
item_code="Test QI DNR Item",
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
# create inward stock entry
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
target=warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
inspection_required=True,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
# create QI from Stock Entry and check the naming series generated.
|
||||
qi = make_quality_inspections(
|
||||
stock_entry.company,
|
||||
stock_entry.doctype,
|
||||
stock_entry.name,
|
||||
stock_entry.as_dict().get("items"),
|
||||
)
|
||||
self.assertEqual(qi[0], "NQC-ST-00001")
|
||||
|
||||
# delete naming rule
|
||||
frappe.delete_doc("Document Naming Rule", qc_naming_rule.name)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -492,7 +492,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Purchase Receipt",
|
||||
"options": "Purchase Receipt",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -616,7 +617,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 15:10:38.373099",
|
||||
"modified": "2026-03-02 14:05:23.116017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -524,9 +524,9 @@ class StockReconciliation(StockController):
|
||||
if abs(difference_amount) > 0:
|
||||
return True
|
||||
|
||||
float_precision = frappe.db.get_default("float_precision") or 3
|
||||
item_dict["rate"] = flt(item_dict.get("rate"), float_precision)
|
||||
item.valuation_rate = flt(item.valuation_rate, float_precision) if item.valuation_rate else None
|
||||
rate_precision = item.precision("valuation_rate")
|
||||
item_dict["rate"] = flt(item_dict.get("rate"), rate_precision)
|
||||
item.valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
|
||||
if (
|
||||
(item.qty is None or item.qty == item_dict.get("qty"))
|
||||
and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
|
||||
|
||||
@@ -17,14 +17,29 @@ from erpnext.stock.utils import is_reposting_item_valuation_in_progress
|
||||
def execute(filters=None):
|
||||
is_reposting_item_valuation_in_progress()
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns(filters)
|
||||
period_columns = get_period_columns(filters)
|
||||
columns = get_columns(period_columns)
|
||||
data = get_data(filters)
|
||||
chart = get_chart_data(columns)
|
||||
chart = get_chart_data(period_columns)
|
||||
|
||||
return columns, data, None, chart
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
def get_period_columns(filters):
|
||||
period_columns = []
|
||||
ranges = get_period_date_ranges(filters)
|
||||
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
|
||||
period_columns.append(
|
||||
{"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120}
|
||||
)
|
||||
|
||||
return period_columns
|
||||
|
||||
|
||||
def get_columns(period_columns):
|
||||
columns = [
|
||||
{"label": _("Item"), "options": "Item", "fieldname": "name", "fieldtype": "Link", "width": 140},
|
||||
{
|
||||
@@ -45,12 +60,7 @@ def get_columns(filters):
|
||||
{"label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 120},
|
||||
]
|
||||
|
||||
ranges = get_period_date_ranges(filters)
|
||||
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
|
||||
columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120})
|
||||
columns.extend(period_columns)
|
||||
|
||||
return columns
|
||||
|
||||
@@ -250,8 +260,8 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_chart_data(columns):
|
||||
labels = [d.get("label") for d in columns[5:]]
|
||||
def get_chart_data(period_columns):
|
||||
labels = [col.get("label") for col in period_columns]
|
||||
chart = {"data": {"labels": labels, "datasets": []}}
|
||||
chart["type"] = "line"
|
||||
|
||||
|
||||
@@ -203,9 +203,18 @@ class StockBalanceReport:
|
||||
.groupby(doctype.voucher_detail_no)
|
||||
)
|
||||
|
||||
data = query.run(as_list=True)
|
||||
if data:
|
||||
self.stock_reco_voucher_wise_count = frappe._dict(data)
|
||||
data = query.run(as_dict=True)
|
||||
if not data:
|
||||
return
|
||||
|
||||
for row in data:
|
||||
if row.count != 1:
|
||||
continue
|
||||
|
||||
current_qty = frappe.db.get_value(
|
||||
"Stock Reconciliation Item", row.voucher_detail_no, "current_qty"
|
||||
)
|
||||
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = current_qty
|
||||
|
||||
def get_sre_reserved_qty_details(self) -> dict:
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
@@ -227,7 +236,8 @@ class StockBalanceReport:
|
||||
if entry.voucher_type == "Stock Reconciliation" and (
|
||||
not entry.batch_no or entry.serial_no or entry.serial_and_batch_bundle
|
||||
):
|
||||
if entry.serial_no and self.stock_reco_voucher_wise_count.get(entry.voucher_detail_no, 0) == 1:
|
||||
if entry.serial_no and entry.voucher_detail_no in self.stock_reco_voucher_wise_count:
|
||||
qty_dict.opening_qty -= self.stock_reco_voucher_wise_count.get(entry.voucher_detail_no, 0)
|
||||
qty_dict.bal_qty = 0.0
|
||||
qty_diff = flt(entry.actual_qty)
|
||||
else:
|
||||
|
||||
@@ -382,6 +382,9 @@ class SerialBatchBundle:
|
||||
|
||||
def submit_serial_and_batch_bundle(self):
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
||||
if self.sle.voucher_detail_no and doc.voucher_detail_no != self.sle.voucher_detail_no:
|
||||
doc.voucher_detail_no = self.sle.voucher_detail_no
|
||||
|
||||
self.validate_actual_qty(doc)
|
||||
|
||||
doc.flags.ignore_voucher_validation = True
|
||||
@@ -441,6 +444,11 @@ class SerialBatchBundle:
|
||||
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
||||
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
||||
|
||||
if sle.voucher_type in ["Stock Entry"] and sle.actual_qty < 0:
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in ["Disassemble", "Material Receipt"]:
|
||||
status = "Inactive"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
|
||||
@@ -1299,7 +1299,7 @@ class update_entries_after:
|
||||
else:
|
||||
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||
ref_doctype = "Packed Item"
|
||||
elif sle == "Subcontracting Receipt":
|
||||
elif sle.voucher_type == "Subcontracting Receipt":
|
||||
ref_doctype = "Subcontracting Receipt Supplied Item"
|
||||
else:
|
||||
ref_doctype = "Purchase Receipt Item Supplied"
|
||||
|
||||
Reference in New Issue
Block a user