mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 00:14:50 +00:00
Merge pull request #47996 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -38,6 +38,11 @@
|
|||||||
"show_taxes_as_table_in_print",
|
"show_taxes_as_table_in_print",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
"show_payment_schedule_in_print",
|
"show_payment_schedule_in_print",
|
||||||
|
"item_price_settings_section",
|
||||||
|
"maintain_same_internal_transaction_rate",
|
||||||
|
"column_break_feyo",
|
||||||
|
"maintain_same_rate_action",
|
||||||
|
"role_to_override_stop_action",
|
||||||
"currency_exchange_section",
|
"currency_exchange_section",
|
||||||
"allow_stale",
|
"allow_stale",
|
||||||
"column_break_yuug",
|
"column_break_yuug",
|
||||||
@@ -540,13 +545,6 @@
|
|||||||
"fieldname": "column_break_xrnd",
|
"fieldname": "column_break_xrnd",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.",
|
|
||||||
"fieldname": "use_sales_invoice_in_pos",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Use Sales Invoice"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Buffered Cursor",
|
"default": "Buffered Cursor",
|
||||||
"fieldname": "receivable_payable_fetch_method",
|
"fieldname": "receivable_payable_fetch_method",
|
||||||
@@ -563,6 +561,37 @@
|
|||||||
"fieldname": "legacy_section",
|
"fieldname": "legacy_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Legacy Fields"
|
"label": "Legacy Fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "maintain_same_internal_transaction_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Maintain Same Rate Throughout Internal Transaction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Stop",
|
||||||
|
"depends_on": "maintain_same_internal_transaction_rate",
|
||||||
|
"fieldname": "maintain_same_rate_action",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
|
||||||
|
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
|
||||||
|
"options": "Stop\nWarn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||||
|
"fieldname": "role_to_override_stop_action",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Role Allowed to Override Stop Action",
|
||||||
|
"options": "Role"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_price_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Item Price Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_feyo",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class AccountsSettings(Document):
|
|||||||
general_ledger_remarks_length: DF.Int
|
general_ledger_remarks_length: DF.Int
|
||||||
ignore_account_closing_balance: DF.Check
|
ignore_account_closing_balance: DF.Check
|
||||||
ignore_is_opening_check_for_reporting: DF.Check
|
ignore_is_opening_check_for_reporting: DF.Check
|
||||||
|
maintain_same_internal_transaction_rate: DF.Check
|
||||||
|
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||||
make_payment_via_journal_entry: DF.Check
|
make_payment_via_journal_entry: DF.Check
|
||||||
merge_similar_account_heads: DF.Check
|
merge_similar_account_heads: DF.Check
|
||||||
over_billing_allowance: DF.Currency
|
over_billing_allowance: DF.Currency
|
||||||
@@ -58,6 +60,7 @@ class AccountsSettings(Document):
|
|||||||
receivable_payable_remarks_length: DF.Int
|
receivable_payable_remarks_length: DF.Int
|
||||||
reconciliation_queue_size: DF.Int
|
reconciliation_queue_size: DF.Int
|
||||||
role_allowed_to_over_bill: DF.Link | None
|
role_allowed_to_over_bill: DF.Link | None
|
||||||
|
role_to_override_stop_action: DF.Link | None
|
||||||
round_row_wise_tax: DF.Check
|
round_row_wise_tax: DF.Check
|
||||||
show_balance_in_coa: DF.Check
|
show_balance_in_coa: DF.Check
|
||||||
show_inclusive_tax_in_print: DF.Check
|
show_inclusive_tax_in_print: DF.Check
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ def get_import_status(docname):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_import_logs(docname: str):
|
def get_import_logs(docname: str):
|
||||||
frappe.has_permission("Bank Statement Import")
|
frappe.has_permission("Bank Statement Import", throw=True)
|
||||||
|
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
"Data Import Log",
|
"Data Import Log",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.model.naming import set_name_from_naming_options
|
from frappe.model.naming import set_name_from_naming_options
|
||||||
from frappe.utils import flt, fmt_money, now
|
from frappe.utils import create_batch, flt, fmt_money, now
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -451,12 +451,15 @@ def rename_gle_sle_docs():
|
|||||||
def rename_temporarily_named_docs(doctype):
|
def rename_temporarily_named_docs(doctype):
|
||||||
"""Rename temporarily named docs using autoname options"""
|
"""Rename temporarily named docs using autoname options"""
|
||||||
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
|
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
|
||||||
for doc in docs_to_rename:
|
autoname = frappe.get_meta(doctype).autoname
|
||||||
oldname = doc.name
|
|
||||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
for batch in create_batch(docs_to_rename, 100):
|
||||||
newname = doc.name
|
for doc in batch:
|
||||||
frappe.db.sql(
|
oldname = doc.name
|
||||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
set_name_from_naming_options(autoname, doc)
|
||||||
(newname, now(), oldname),
|
newname = doc.name
|
||||||
auto_commit=True,
|
frappe.db.sql(
|
||||||
)
|
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||||
|
(newname, now(), oldname),
|
||||||
|
)
|
||||||
|
frappe.db.commit()
|
||||||
|
|||||||
@@ -2806,6 +2806,17 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
|
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
|
||||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||||
|
|
||||||
|
def test_discount_percentage_not_set_when_amount_is_manually_set(self):
|
||||||
|
pi = make_purchase_invoice(do_not_save=True)
|
||||||
|
discount_amount = 7
|
||||||
|
pi.discount_amount = discount_amount
|
||||||
|
pi.save()
|
||||||
|
self.assertEqual(pi.additional_discount_percentage, None)
|
||||||
|
pi.set_posting_time = 1
|
||||||
|
pi.posting_date = add_days(today(), -1)
|
||||||
|
pi.save()
|
||||||
|
self.assertEqual(pi.discount_amount, discount_amount)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -64,6 +64,26 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"maintain_same_internal_transaction_rate": 1, "maintain_same_rate_action": "Stop"},
|
||||||
|
)
|
||||||
|
def test_invalid_rate_without_override(self):
|
||||||
|
from frappe import ValidationError
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
|
||||||
|
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100
|
||||||
|
)
|
||||||
|
pi = make_inter_company_purchase_invoice(si.name)
|
||||||
|
pi.items[0].rate = 120
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError) as e:
|
||||||
|
pi.insert()
|
||||||
|
pi.submit()
|
||||||
|
self.assertIn("Rate must be same", str(e.exception))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
@@ -4441,6 +4461,7 @@ def create_sales_invoice(**args):
|
|||||||
si.conversion_rate = args.conversion_rate or 1
|
si.conversion_rate = args.conversion_rate or 1
|
||||||
si.naming_series = args.naming_series or "T-SINV-"
|
si.naming_series = args.naming_series or "T-SINV-"
|
||||||
si.cost_center = args.parent_cost_center
|
si.cost_center = args.parent_cost_center
|
||||||
|
si.is_internal_customer = args.is_internal_customer or 0
|
||||||
|
|
||||||
bundle_id = None
|
bundle_id = None
|
||||||
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
|
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
|
||||||
@@ -4643,6 +4664,12 @@ def create_internal_parties():
|
|||||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
create_internal_supplier(
|
||||||
|
supplier_name="_Test Internal Supplier 3",
|
||||||
|
represents_company="_Test Company",
|
||||||
|
allowed_to_interact_with="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
|
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
|
||||||
if not frappe.db.exists("Supplier", supplier_name):
|
if not frappe.db.exists("Supplier", supplier_name):
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
|
|
||||||
{% if(!filters.show_future_payments) { %}
|
{% if(!filters.show_future_payments) { %}
|
||||||
<td>
|
<td>
|
||||||
{% if(!(filters.party)) { %}
|
{% if(!filters.party?.length) { %}
|
||||||
{%= data[i]["party"] %}
|
{%= data[i]["party"] %}
|
||||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||||
<br> {%= data[i]["customer_name"] %}
|
<br> {%= data[i]["customer_name"] %}
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
{% if(data[i]["party"]|| " ") { %}
|
{% if(data[i]["party"]|| " ") { %}
|
||||||
{% if(!data[i]["is_total_row"]) { %}
|
{% if(!data[i]["is_total_row"]) { %}
|
||||||
<td>
|
<td>
|
||||||
{% if(!(filters.party)) { %}
|
{% if(!filters.party?.length) { %}
|
||||||
{%= data[i]["party"] %}
|
{%= data[i]["party"] %}
|
||||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||||
<br> {%= data[i]["customer_name"] %}
|
<br> {%= data[i]["customer_name"] %}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, query_builder, scrub
|
from frappe import _, qb, query_builder, scrub
|
||||||
|
from frappe.desk.reportview import build_match_conditions
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.functions import Date, Substring, Sum
|
from frappe.query_builder.functions import Date, Substring, Sum
|
||||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
@@ -126,7 +127,7 @@ class ReceivablePayableReport:
|
|||||||
self.build_data()
|
self.build_data()
|
||||||
|
|
||||||
def fetch_ple_in_buffered_cursor(self):
|
def fetch_ple_in_buffered_cursor(self):
|
||||||
query, param = self.ple_query.walk()
|
query, param = self.ple_query
|
||||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||||
|
|
||||||
for ple in self.ple_entries:
|
for ple in self.ple_entries:
|
||||||
@@ -140,7 +141,7 @@ class ReceivablePayableReport:
|
|||||||
|
|
||||||
def fetch_ple_in_unbuffered_cursor(self):
|
def fetch_ple_in_unbuffered_cursor(self):
|
||||||
self.ple_entries = []
|
self.ple_entries = []
|
||||||
query, param = self.ple_query.walk()
|
query, param = self.ple_query
|
||||||
with frappe.db.unbuffered_cursor():
|
with frappe.db.unbuffered_cursor():
|
||||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||||
@@ -449,16 +450,14 @@ class ReceivablePayableReport:
|
|||||||
self.invoice_details = frappe._dict()
|
self.invoice_details = frappe._dict()
|
||||||
if self.account_type == "Receivable":
|
if self.account_type == "Receivable":
|
||||||
# nosemgrep
|
# nosemgrep
|
||||||
si_list = frappe.db.sql(
|
si_list = frappe.get_list(
|
||||||
"""
|
"Sales Invoice",
|
||||||
select name, due_date, po_no
|
filters={
|
||||||
from `tabSales Invoice`
|
"posting_date": ("<=", self.filters.report_date),
|
||||||
where posting_date <= %s
|
"company": self.filters.company,
|
||||||
and company = %s
|
"docstatus": 1,
|
||||||
and docstatus = 1
|
},
|
||||||
""",
|
fields=["name", "due_date", "po_no"],
|
||||||
(self.filters.report_date, self.filters.company),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
for d in si_list:
|
for d in si_list:
|
||||||
self.invoice_details.setdefault(d.name, d)
|
self.invoice_details.setdefault(d.name, d)
|
||||||
@@ -481,33 +480,29 @@ class ReceivablePayableReport:
|
|||||||
|
|
||||||
if self.account_type == "Payable":
|
if self.account_type == "Payable":
|
||||||
# nosemgrep
|
# nosemgrep
|
||||||
for pi in frappe.db.sql(
|
invoices = frappe.get_list(
|
||||||
"""
|
"Purchase Invoice",
|
||||||
select name, due_date, bill_no, bill_date
|
filters={
|
||||||
from `tabPurchase Invoice`
|
"posting_date": ("<=", self.filters.report_date),
|
||||||
where
|
"company": self.filters.company,
|
||||||
posting_date <= %s
|
"docstatus": 1,
|
||||||
and company = %s
|
},
|
||||||
and docstatus = 1
|
fields=["name", "due_date", "bill_no", "bill_date"],
|
||||||
""",
|
)
|
||||||
(self.filters.report_date, self.filters.company),
|
|
||||||
as_dict=1,
|
for pi in invoices:
|
||||||
):
|
|
||||||
self.invoice_details.setdefault(pi.name, pi)
|
self.invoice_details.setdefault(pi.name, pi)
|
||||||
|
|
||||||
# Invoices booked via Journal Entries
|
# Invoices booked via Journal Entries
|
||||||
# nosemgrep
|
# nosemgrep
|
||||||
journal_entries = frappe.db.sql(
|
journal_entries = frappe.get_list(
|
||||||
"""
|
"Journal Entry",
|
||||||
select name, due_date, bill_no, bill_date
|
filters={
|
||||||
from `tabJournal Entry`
|
"posting_date": ("<=", self.filters.report_date),
|
||||||
where
|
"company": self.filters.company,
|
||||||
posting_date <= %s
|
"docstatus": 1,
|
||||||
and company = %s
|
},
|
||||||
and docstatus = 1
|
fields=["name", "due_date", "bill_no", "bill_date"],
|
||||||
""",
|
|
||||||
(self.filters.report_date, self.filters.company),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for je in journal_entries:
|
for je in journal_entries:
|
||||||
@@ -856,12 +851,18 @@ class ReceivablePayableReport:
|
|||||||
else:
|
else:
|
||||||
query = query.select(ple.remarks)
|
query = query.select(ple.remarks)
|
||||||
|
|
||||||
if self.filters.get("group_by_party"):
|
query, param = query.walk()
|
||||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
|
||||||
else:
|
|
||||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
|
||||||
|
|
||||||
self.ple_query = query
|
match_conditions = build_match_conditions("Payment Ledger Entry")
|
||||||
|
if match_conditions:
|
||||||
|
query += " AND " + match_conditions
|
||||||
|
|
||||||
|
if self.filters.get("group_by_party"):
|
||||||
|
query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`"
|
||||||
|
else:
|
||||||
|
query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`"
|
||||||
|
|
||||||
|
self.ple_query = (query, param)
|
||||||
|
|
||||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||||
if self.filters.get("sales_person"):
|
if self.filters.get("sales_person"):
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.query_reports["Calculated Discount Mismatch"] = {
|
||||||
|
// filters: [
|
||||||
|
// {
|
||||||
|
// "fieldname": "my_filter",
|
||||||
|
// "label": __("My Filter"),
|
||||||
|
// "fieldtype": "Data",
|
||||||
|
// "reqd": 1,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// };
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"add_translate_data": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2025-06-06 17:09:50.681090",
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"letter_head": "",
|
||||||
|
"letterhead": null,
|
||||||
|
"modified": "2025-06-06 18:09:18.221911",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Calculated Discount Mismatch",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Version",
|
||||||
|
"report_name": "Calculated Discount Mismatch",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "System Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Administrator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Accounts Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Accounts User"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeout": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.query_builder import Order, Tuple
|
||||||
|
from frappe.utils.formatters import format_value
|
||||||
|
|
||||||
|
AFFECTED_DOCTYPES = frozenset(
|
||||||
|
(
|
||||||
|
"POS Invoice",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Sales Invoice",
|
||||||
|
"Purchase Order",
|
||||||
|
"Supplier Quotation",
|
||||||
|
"Quotation",
|
||||||
|
"Sales Order",
|
||||||
|
"Delivery Note",
|
||||||
|
"Purchase Receipt",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LAST_MODIFIED_DATE_THRESHOLD = "2025-05-30"
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data()
|
||||||
|
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"fieldname": "doctype",
|
||||||
|
"label": _("Transaction Type"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "DocType",
|
||||||
|
"width": 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "docname",
|
||||||
|
"label": _("Transaction Name"),
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "doctype",
|
||||||
|
"width": 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "actual_discount_percentage",
|
||||||
|
"label": _("Discount Percentage in Transaction"),
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"width": 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "actual_discount_amount",
|
||||||
|
"label": _("Discount Amount in Transaction"),
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "suspected_discount_amount",
|
||||||
|
"label": _("Suspected Discount Amount"),
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 180,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
transactions_with_discount_percentage = {}
|
||||||
|
|
||||||
|
for doctype in AFFECTED_DOCTYPES:
|
||||||
|
transactions = get_transactions_with_discount_percentage(doctype)
|
||||||
|
|
||||||
|
for transaction in transactions:
|
||||||
|
transactions_with_discount_percentage[(doctype, transaction.name)] = transaction
|
||||||
|
|
||||||
|
if not transactions_with_discount_percentage:
|
||||||
|
return []
|
||||||
|
|
||||||
|
VERSION = frappe.qb.DocType("Version")
|
||||||
|
|
||||||
|
versions = (
|
||||||
|
frappe.qb.from_(VERSION)
|
||||||
|
.select(VERSION.ref_doctype, VERSION.docname, VERSION.data)
|
||||||
|
.where(VERSION.creation > LAST_MODIFIED_DATE_THRESHOLD)
|
||||||
|
.where(Tuple(VERSION.ref_doctype, VERSION.docname).isin(list(transactions_with_discount_percentage)))
|
||||||
|
.where(
|
||||||
|
VERSION.data.like('%"discount\\_amount"%')
|
||||||
|
| VERSION.data.like('%"additional\\_discount\\_percentage"%')
|
||||||
|
)
|
||||||
|
.orderby(VERSION.creation, order=Order.desc)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not versions:
|
||||||
|
return []
|
||||||
|
|
||||||
|
version_map = {}
|
||||||
|
for version in versions:
|
||||||
|
key = (version.ref_doctype, version.docname)
|
||||||
|
if key not in version_map:
|
||||||
|
version_map[key] = []
|
||||||
|
|
||||||
|
version_map[key].append(version.data)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
discount_amount_field_map = {
|
||||||
|
doctype: frappe.get_meta(doctype).get_field("discount_amount") for doctype in AFFECTED_DOCTYPES
|
||||||
|
}
|
||||||
|
for doc, versions in version_map.items():
|
||||||
|
for version_data in versions:
|
||||||
|
if '"additional_discount_percentage"' in version_data:
|
||||||
|
# don't consider doc if additional_discount_percentage is changed in newest version
|
||||||
|
break
|
||||||
|
|
||||||
|
version_data = json.loads(version_data)
|
||||||
|
changed_values = version_data.get("changed")
|
||||||
|
if not changed_values:
|
||||||
|
continue
|
||||||
|
|
||||||
|
discount_values = next((row for row in changed_values if row[0] == "discount_amount"), None)
|
||||||
|
if not discount_values:
|
||||||
|
continue
|
||||||
|
|
||||||
|
old = discount_values[1]
|
||||||
|
new = discount_values[2]
|
||||||
|
doctype = doc[0]
|
||||||
|
doc_values = transactions_with_discount_percentage.get(doc)
|
||||||
|
formatted_discount_amount = format_value(
|
||||||
|
doc_values.discount_amount,
|
||||||
|
df=discount_amount_field_map[doctype],
|
||||||
|
currency=doc_values.currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
if new != formatted_discount_amount:
|
||||||
|
# if the discount amount in the version is not equal to the current value, skip
|
||||||
|
break
|
||||||
|
|
||||||
|
data.append(
|
||||||
|
{
|
||||||
|
"doctype": doctype,
|
||||||
|
"docname": doc_values.name,
|
||||||
|
"actual_discount_percentage": doc_values.additional_discount_percentage,
|
||||||
|
"actual_discount_amount": new,
|
||||||
|
"suspected_discount_amount": old,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_transactions_with_discount_percentage(doctype):
|
||||||
|
transactions = frappe.get_all(
|
||||||
|
doctype,
|
||||||
|
fields=[
|
||||||
|
"name",
|
||||||
|
"currency",
|
||||||
|
"additional_discount_percentage",
|
||||||
|
"discount_amount",
|
||||||
|
],
|
||||||
|
filters={
|
||||||
|
"docstatus": ["<", 2],
|
||||||
|
"additional_discount_percentage": [">", 0],
|
||||||
|
"discount_amount": ["!=", 0],
|
||||||
|
"modified": [">", LAST_MODIFIED_DATE_THRESHOLD],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return transactions
|
||||||
@@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
from frappe.utils.xlsxutils import handle_html
|
from frappe.utils.xlsxutils import handle_html
|
||||||
from pypika import Order
|
from pypika import Order
|
||||||
|
|
||||||
@@ -375,7 +376,12 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
|||||||
query = query.where(sii.item_code == filters.get("item_code"))
|
query = query.where(sii.item_code == filters.get("item_code"))
|
||||||
|
|
||||||
if filters.get("item_group"):
|
if filters.get("item_group"):
|
||||||
query = query.where(sii.item_group == filters.get("item_group"))
|
if frappe.db.get_value("Item Group", filters.get("item_group"), "is_group"):
|
||||||
|
item_groups = get_descendants_of("Item Group", filters.get("item_group"))
|
||||||
|
item_groups.append(filters.get("item_group"))
|
||||||
|
query = query.where(sii.item_group.isin(item_groups))
|
||||||
|
else:
|
||||||
|
query = query.where(sii.item_group == filters.get("item_group"))
|
||||||
|
|
||||||
if filters.get("income_account"):
|
if filters.get("income_account"):
|
||||||
query = query.where(
|
query = query.where(
|
||||||
|
|||||||
@@ -205,8 +205,8 @@
|
|||||||
"fieldname": "purchase_date",
|
"fieldname": "purchase_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Purchase Date",
|
"label": "Purchase Date",
|
||||||
"mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
||||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "disposal_date",
|
"fieldname": "disposal_date",
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Asset(AccountsController):
|
|||||||
opening_number_of_booked_depreciations: DF.Int
|
opening_number_of_booked_depreciations: DF.Int
|
||||||
policy_number: DF.Data | None
|
policy_number: DF.Data | None
|
||||||
purchase_amount: DF.Currency
|
purchase_amount: DF.Currency
|
||||||
purchase_date: DF.Date | None
|
purchase_date: DF.Date
|
||||||
purchase_invoice: DF.Link | None
|
purchase_invoice: DF.Link | None
|
||||||
purchase_invoice_item: DF.Data | None
|
purchase_invoice_item: DF.Data | None
|
||||||
purchase_receipt: DF.Link | None
|
purchase_receipt: DF.Link | None
|
||||||
|
|||||||
@@ -798,7 +798,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
|
|||||||
|
|
||||||
idx = 1
|
idx = 1
|
||||||
if finance_book:
|
if finance_book:
|
||||||
for d in asset.finance_books:
|
for d in asset_doc.finance_books:
|
||||||
if d.finance_book == finance_book:
|
if d.finance_book == finance_book:
|
||||||
idx = d.idx
|
idx = d.idx
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
|||||||
width: "80",
|
width: "80",
|
||||||
options: "Company",
|
options: "Company",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frappe.defaults.get_user_default("company"),
|
default: frappe.defaults.get_user_default("Company"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "from_date",
|
fieldname: "from_date",
|
||||||
|
|||||||
11
erpnext/change_log/v15/v15_64_0.md
Normal file
11
erpnext/change_log/v15/v15_64_0.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
There was a bug in the **Additional Discount** functionality of ERPNext in **v15.64.0**. This has since been fixed.
|
||||||
|
|
||||||
|
**If you've updated from a version older than v15.64.0, no action is needed on your side.**
|
||||||
|
|
||||||
|
If you're updating from v15.64.0, the **Additional Discount Amount** in some transactions may differ from the value you entered. This only affects cases where **Additional Discount Amount** is manually entered. If it is computed from **Additional Discount Percentage** entered by you, there shouldn't be any issue.
|
||||||
|
|
||||||
|
This report can help identify such transactions: [Calculated Discount Mismatch](/app/query-report/Calculated%20Discount%20Mismatch)
|
||||||
|
|
||||||
|
Please review and amend these as necessary.
|
||||||
|
|
||||||
|
We apologize for the inconvenience caused.
|
||||||
@@ -230,6 +230,8 @@ class AccountsController(TransactionBase):
|
|||||||
self.validate_party_accounts()
|
self.validate_party_accounts()
|
||||||
|
|
||||||
self.validate_inter_company_reference()
|
self.validate_inter_company_reference()
|
||||||
|
# validate inter company transaction rate
|
||||||
|
self.validate_internal_transaction()
|
||||||
|
|
||||||
self.disable_pricing_rule_on_internal_transfer()
|
self.disable_pricing_rule_on_internal_transfer()
|
||||||
self.disable_tax_included_prices_for_internal_transfer()
|
self.disable_tax_included_prices_for_internal_transfer()
|
||||||
@@ -740,6 +742,91 @@ class AccountsController(TransactionBase):
|
|||||||
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
|
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
|
||||||
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
|
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
|
||||||
|
|
||||||
|
def validate_internal_transaction(self):
|
||||||
|
if not cint(
|
||||||
|
frappe.db.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate")
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
doctypes_list = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||||
|
|
||||||
|
if self.doctype in doctypes_list and (
|
||||||
|
self.get("is_internal_customer") or self.get("is_internal_supplier")
|
||||||
|
):
|
||||||
|
self.validate_internal_transaction_based_on_voucher_type()
|
||||||
|
|
||||||
|
def validate_internal_transaction_based_on_voucher_type(self):
|
||||||
|
order = ["Sales Order", "Purchase Order"]
|
||||||
|
invoice = ["Sales Invoice", "Purchase Invoice"]
|
||||||
|
|
||||||
|
if self.doctype in order and self.get("inter_company_order_reference"):
|
||||||
|
# Fetch the linked order
|
||||||
|
linked_doctype = "Sales Order" if self.doctype == "Purchase Order" else "Purchase Order"
|
||||||
|
self.validate_line_items(
|
||||||
|
linked_doctype,
|
||||||
|
"sales_order" if linked_doctype == "Sales Order" else "purchase_order",
|
||||||
|
"sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item",
|
||||||
|
)
|
||||||
|
elif self.doctype in invoice and self.get("inter_company_invoice_reference"):
|
||||||
|
# Fetch the linked invoice
|
||||||
|
linked_doctype = "Sales Invoice" if self.doctype == "Purchase Invoice" else "Purchase Invoice"
|
||||||
|
self.validate_line_items(
|
||||||
|
linked_doctype,
|
||||||
|
"sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice",
|
||||||
|
"sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item",
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_line_items(self, ref_dt, ref_dn_field, ref_link_field):
|
||||||
|
action, role_allowed_to_override = frappe.get_cached_value(
|
||||||
|
"Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||||
|
)
|
||||||
|
|
||||||
|
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
|
||||||
|
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
|
||||||
|
|
||||||
|
stop_actions = []
|
||||||
|
|
||||||
|
for d in self.get("items"):
|
||||||
|
if d.get(ref_link_field):
|
||||||
|
ref_rate = reference_details.get(d.get(ref_link_field))
|
||||||
|
if ref_rate is not None and abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
||||||
|
if action == "Stop":
|
||||||
|
user_roles = [
|
||||||
|
r["role"]
|
||||||
|
for r in frappe.get_all(
|
||||||
|
"Has Role", filters={"parent": frappe.session.user}, fields=["role"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if role_allowed_to_override not in user_roles:
|
||||||
|
stop_actions.append(
|
||||||
|
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||||
|
d.idx,
|
||||||
|
ref_dt,
|
||||||
|
self.inter_company_invoice_reference
|
||||||
|
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
|
||||||
|
else d.get(ref_dn_field),
|
||||||
|
d.rate,
|
||||||
|
ref_rate,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||||
|
d.idx,
|
||||||
|
ref_dt,
|
||||||
|
self.inter_company_invoice_reference
|
||||||
|
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
|
||||||
|
else d.get(ref_dn_field),
|
||||||
|
d.rate,
|
||||||
|
ref_rate,
|
||||||
|
),
|
||||||
|
title=_("Warning"),
|
||||||
|
indicator="orange",
|
||||||
|
)
|
||||||
|
|
||||||
|
if stop_actions:
|
||||||
|
frappe.throw(stop_actions, as_list=True)
|
||||||
|
|
||||||
def disable_pricing_rule_on_internal_transfer(self):
|
def disable_pricing_rule_on_internal_transfer(self):
|
||||||
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||||
self.ignore_pricing_rule = 1
|
self.ignore_pricing_rule = 1
|
||||||
@@ -1151,6 +1238,8 @@ class AccountsController(TransactionBase):
|
|||||||
with temporary_flag("company", self.company):
|
with temporary_flag("company", self.company):
|
||||||
update_gl_dict_with_regional_fields(self, gl_dict)
|
update_gl_dict_with_regional_fields(self, gl_dict)
|
||||||
|
|
||||||
|
update_gl_dict_with_app_based_fields(self, gl_dict)
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions()
|
accounting_dimensions = get_accounting_dimensions()
|
||||||
dimension_dict = frappe._dict()
|
dimension_dict = frappe._dict()
|
||||||
|
|
||||||
@@ -3937,3 +4026,8 @@ def validate_einvoice_fields(doc):
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def update_gl_dict_with_regional_fields(doc, gl_dict):
|
def update_gl_dict_with_regional_fields(doc, gl_dict):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def update_gl_dict_with_app_based_fields(doc, gl_dict):
|
||||||
|
for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
|
||||||
|
frappe.get_attr(method)(doc, gl_dict)
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ def get_columns(filters, trans):
|
|||||||
columns = (
|
columns = (
|
||||||
based_on_details["based_on_cols"]
|
based_on_details["based_on_cols"]
|
||||||
+ period_cols
|
+ period_cols
|
||||||
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"]
|
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency/currency:120"]
|
||||||
)
|
)
|
||||||
if group_by_cols:
|
if group_by_cols:
|
||||||
columns = (
|
columns = (
|
||||||
based_on_details["based_on_cols"]
|
based_on_details["based_on_cols"]
|
||||||
+ group_by_cols
|
+ group_by_cols
|
||||||
+ period_cols
|
+ period_cols
|
||||||
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"]
|
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency/currency:120"]
|
||||||
)
|
)
|
||||||
|
|
||||||
conditions = {
|
conditions = {
|
||||||
@@ -157,7 +157,7 @@ def get_data(filters, conditions):
|
|||||||
|
|
||||||
# get data for group_by filter
|
# get data for group_by filter
|
||||||
row1 = frappe.db.sql(
|
row1 = frappe.db.sql(
|
||||||
""" select {} , {} from `tab{}` t1, `tab{} Item` t2 {}
|
""" select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||||
where t2.parent = t1.name and t1.company = {} and {} between {} and {}
|
where t2.parent = t1.name and t1.company = {} and {} between {} and {}
|
||||||
and t1.docstatus = 1 and {} = {} and {} = {} {} {}
|
and t1.docstatus = 1 and {} = {} and {} = {} {} {}
|
||||||
""".format(
|
""".format(
|
||||||
@@ -182,6 +182,7 @@ def get_data(filters, conditions):
|
|||||||
)
|
)
|
||||||
|
|
||||||
des[ind] = row[i][0]
|
des[ind] = row[i][0]
|
||||||
|
des[ind - 1] = row1[0][0]
|
||||||
|
|
||||||
for j in range(1, len(conditions["columns"]) - inc):
|
for j in range(1, len(conditions["columns"]) - inc):
|
||||||
des[j + inc] = row1[0][j]
|
des[j + inc] = row1[0][j]
|
||||||
@@ -236,7 +237,7 @@ def period_wise_columns_query(filters, trans):
|
|||||||
else:
|
else:
|
||||||
pwc = [
|
pwc = [
|
||||||
_(filters.get("fiscal_year")) + " (" + _("Qty") + "):Float:120",
|
_(filters.get("fiscal_year")) + " (" + _("Qty") + "):Float:120",
|
||||||
_(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency:120",
|
_(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency/currency:120",
|
||||||
]
|
]
|
||||||
query_details = " SUM(t2.stock_qty), SUM(t2.base_net_amount),"
|
query_details = " SUM(t2.stock_qty), SUM(t2.base_net_amount),"
|
||||||
|
|
||||||
@@ -248,12 +249,17 @@ def get_period_wise_columns(bet_dates, period, pwc):
|
|||||||
if period == "Monthly":
|
if period == "Monthly":
|
||||||
pwc += [
|
pwc += [
|
||||||
_(get_mon(bet_dates[0])) + " (" + _("Qty") + "):Float:120",
|
_(get_mon(bet_dates[0])) + " (" + _("Qty") + "):Float:120",
|
||||||
_(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency:120",
|
_(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency/currency:120",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
pwc += [
|
pwc += [
|
||||||
_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Qty") + "):Float:120",
|
_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Qty") + "):Float:120",
|
||||||
_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Amt") + "):Currency:120",
|
_(get_mon(bet_dates[0]))
|
||||||
|
+ "-"
|
||||||
|
+ _(get_mon(bet_dates[1]))
|
||||||
|
+ " ("
|
||||||
|
+ _("Amt")
|
||||||
|
+ "):Currency/currency:120",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -375,6 +381,9 @@ def based_wise_columns_query(based_on, trans):
|
|||||||
else:
|
else:
|
||||||
frappe.throw(_("Project-wise data is not available for Quotation"))
|
frappe.throw(_("Project-wise data is not available for Quotation"))
|
||||||
|
|
||||||
|
based_on_details["based_on_select"] += "t1.currency,"
|
||||||
|
based_on_details["based_on_cols"].append("Currency:Link/Currency:120")
|
||||||
|
|
||||||
return based_on_details
|
return based_on_details
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.po_items && frm.doc.status !== "Closed") {
|
let items = frm.events.get_items_for_work_order(frm);
|
||||||
|
|
||||||
|
if (items?.length && frm.doc.status !== "Closed") {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Work Order / Subcontract PO"),
|
__("Work Order / Subcontract PO"),
|
||||||
() => {
|
() => {
|
||||||
@@ -193,6 +195,24 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
set_field_options("projected_qty_formula", projected_qty_formula);
|
set_field_options("projected_qty_formula", projected_qty_formula);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_items_for_work_order(frm) {
|
||||||
|
let items = frm.doc.po_items;
|
||||||
|
if (frm.doc.sub_assembly_items?.length) {
|
||||||
|
items = [...items, ...frm.doc.sub_assembly_items];
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_items =
|
||||||
|
items.filter((item) => {
|
||||||
|
if (item.pending_qty) {
|
||||||
|
return item.pending_qty > item.ordered_qty;
|
||||||
|
} else {
|
||||||
|
return item.qty > (item.received_qty || item.ordered_qty);
|
||||||
|
}
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return has_items;
|
||||||
|
},
|
||||||
|
|
||||||
close_open_production_plan(frm, close = false) {
|
close_open_production_plan(frm, close = false) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "set_status",
|
method: "set_status",
|
||||||
|
|||||||
@@ -751,7 +751,14 @@ class ProductionPlan(Document):
|
|||||||
"company": self.get("company"),
|
"company": self.get("company"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flt(row.qty) <= flt(row.ordered_qty):
|
||||||
|
continue
|
||||||
|
|
||||||
self.prepare_data_for_sub_assembly_items(row, work_order_data)
|
self.prepare_data_for_sub_assembly_items(row, work_order_data)
|
||||||
|
|
||||||
|
if work_order_data.get("qty") <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
work_order = self.create_work_order(work_order_data)
|
work_order = self.create_work_order(work_order_data)
|
||||||
if work_order:
|
if work_order:
|
||||||
wo_list.append(work_order)
|
wo_list.append(work_order)
|
||||||
@@ -771,6 +778,8 @@ class ProductionPlan(Document):
|
|||||||
if row.get(field):
|
if row.get(field):
|
||||||
wo_data[field] = row.get(field)
|
wo_data[field] = row.get(field)
|
||||||
|
|
||||||
|
wo_data["qty"] = flt(row.get("qty")) - flt(row.get("ordered_qty"))
|
||||||
|
|
||||||
wo_data.update(
|
wo_data.update(
|
||||||
{
|
{
|
||||||
"use_multi_level_bom": 0,
|
"use_multi_level_bom": 0,
|
||||||
@@ -1242,6 +1251,7 @@ def get_subitems(
|
|||||||
item_default.default_warehouse,
|
item_default.default_warehouse,
|
||||||
item.purchase_uom,
|
item.purchase_uom,
|
||||||
item_uom.conversion_factor,
|
item_uom.conversion_factor,
|
||||||
|
bom.item.as_("main_bom_item"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(bom.name == bom_no)
|
(bom.name == bom_no)
|
||||||
@@ -1355,6 +1365,7 @@ def get_material_request_items(
|
|||||||
"sales_order": sales_order,
|
"sales_order": sales_order,
|
||||||
"description": row.get("description"),
|
"description": row.get("description"),
|
||||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
||||||
|
"main_bom_item": row.get("main_bom_item"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1693,6 +1693,63 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
self.assertEqual(mr_items[0].get("quantity"), 80)
|
self.assertEqual(mr_items[0].get("quantity"), 80)
|
||||||
self.assertEqual(mr_items[1].get("quantity"), 70)
|
self.assertEqual(mr_items[1].get("quantity"), 70)
|
||||||
|
|
||||||
|
def test_production_plan_for_partial_sub_assembly_items(self):
|
||||||
|
from erpnext.controllers.status_updater import OverAllowanceError
|
||||||
|
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
|
||||||
|
create_subcontracting_bom,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.flags.test_print = False
|
||||||
|
|
||||||
|
fg_wo_item = "Test Motherboard 11"
|
||||||
|
bom_tree_1 = {"Test Laptop 11": {fg_wo_item: {"Test Motherboard Wires 11": {}}}}
|
||||||
|
create_nested_bom(bom_tree_1, prefix="")
|
||||||
|
|
||||||
|
plan = create_production_plan(
|
||||||
|
item_code="Test Laptop 11",
|
||||||
|
planned_qty=10,
|
||||||
|
use_multi_level_bom=1,
|
||||||
|
do_not_submit=True,
|
||||||
|
company="_Test Company",
|
||||||
|
skip_getting_mr_items=True,
|
||||||
|
)
|
||||||
|
plan.get_sub_assembly_items()
|
||||||
|
plan.submit()
|
||||||
|
plan.make_work_order()
|
||||||
|
|
||||||
|
work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name")
|
||||||
|
wo_doc = frappe.get_doc("Work Order", work_order)
|
||||||
|
|
||||||
|
wo_doc.qty = 5.0
|
||||||
|
wo_doc.skip_transfer = 1
|
||||||
|
wo_doc.from_wip_warehouse = 1
|
||||||
|
wo_doc.wip_warehouse = "_Test Warehouse - _TC"
|
||||||
|
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
|
||||||
|
wo_doc.submit()
|
||||||
|
|
||||||
|
plan.reload()
|
||||||
|
|
||||||
|
for row in plan.sub_assembly_items:
|
||||||
|
self.assertEqual(row.ordered_qty, 5.0)
|
||||||
|
|
||||||
|
plan.make_work_order()
|
||||||
|
|
||||||
|
work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name")
|
||||||
|
wo_doc = frappe.get_doc("Work Order", work_order)
|
||||||
|
self.assertEqual(wo_doc.qty, 5.0)
|
||||||
|
|
||||||
|
wo_doc.skip_transfer = 1
|
||||||
|
wo_doc.from_wip_warehouse = 1
|
||||||
|
wo_doc.wip_warehouse = "_Test Warehouse - _TC"
|
||||||
|
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
|
||||||
|
wo_doc.submit()
|
||||||
|
|
||||||
|
plan.reload()
|
||||||
|
|
||||||
|
for row in plan.sub_assembly_items:
|
||||||
|
self.assertEqual(row.ordered_qty, 10.0)
|
||||||
|
|
||||||
|
|
||||||
def create_production_plan(**args):
|
def create_production_plan(**args):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"purchase_order",
|
"purchase_order",
|
||||||
"production_plan_item",
|
"production_plan_item",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
|
"ordered_qty",
|
||||||
"received_qty",
|
"received_qty",
|
||||||
"indent",
|
"indent",
|
||||||
"section_break_19",
|
"section_break_19",
|
||||||
@@ -204,12 +205,19 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Produced Qty",
|
"label": "Produced Qty",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "ordered_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Ordered Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-27 13:45:17.422435",
|
"modified": "2025-06-10 13:36:24.759101",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Sub Assembly Item",
|
"name": "Production Plan Sub Assembly Item",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ProductionPlanSubAssemblyItem(Document):
|
|||||||
fg_warehouse: DF.Link | None
|
fg_warehouse: DF.Link | None
|
||||||
indent: DF.Int
|
indent: DF.Int
|
||||||
item_name: DF.Data | None
|
item_name: DF.Data | None
|
||||||
|
ordered_qty: DF.Float
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parent_item_code: DF.Link | None
|
parent_item_code: DF.Link | None
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
|
|||||||
@@ -762,22 +762,34 @@ class WorkOrder(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_ordered_qty(self):
|
def update_ordered_qty(self):
|
||||||
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
|
if self.production_plan and (self.production_plan_item or self.production_plan_sub_assembly_item):
|
||||||
table = frappe.qb.DocType("Work Order")
|
table = frappe.qb.DocType("Work Order")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
.select(Sum(table.qty))
|
.select(Sum(table.qty))
|
||||||
.where(
|
.where((table.production_plan == self.production_plan) & (table.docstatus == 1))
|
||||||
(table.production_plan == self.production_plan)
|
)
|
||||||
& (table.production_plan_item == self.production_plan_item)
|
|
||||||
& (table.docstatus == 1)
|
|
||||||
)
|
|
||||||
).run()
|
|
||||||
|
|
||||||
|
if self.production_plan_item:
|
||||||
|
query = query.where(table.production_plan_item == self.production_plan_item)
|
||||||
|
elif self.production_plan_sub_assembly_item:
|
||||||
|
query = query.where(
|
||||||
|
table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.run()
|
||||||
qty = flt(query[0][0]) if query else 0
|
qty = flt(query[0][0]) if query else 0
|
||||||
|
|
||||||
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
|
if self.production_plan_item:
|
||||||
|
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
|
||||||
|
elif self.production_plan_sub_assembly_item:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Production Plan Sub Assembly Item",
|
||||||
|
self.production_plan_sub_assembly_item,
|
||||||
|
"ordered_qty",
|
||||||
|
qty,
|
||||||
|
)
|
||||||
|
|
||||||
doc = frappe.get_doc("Production Plan", self.production_plan)
|
doc = frappe.get_doc("Production Plan", self.production_plan)
|
||||||
doc.set_status()
|
doc.set_status()
|
||||||
@@ -1503,20 +1515,20 @@ def stop_unstop(work_order, status):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def query_sales_order(production_item):
|
def query_sales_order(production_item: str) -> list[str]:
|
||||||
out = frappe.db.sql_list(
|
return frappe.get_list(
|
||||||
"""
|
"Sales Order",
|
||||||
select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item
|
filters=[
|
||||||
where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1
|
["Sales Order", "docstatus", "=", 1],
|
||||||
union
|
],
|
||||||
select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item
|
or_filters=[
|
||||||
where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1
|
["Sales Order Item", "item_code", "=", production_item],
|
||||||
""",
|
["Packed Item", "item_code", "=", production_item],
|
||||||
(production_item, production_item),
|
],
|
||||||
|
pluck="name",
|
||||||
|
distinct=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_job_card(work_order, operations):
|
def make_job_card(work_order, operations):
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ def get_bom_stock(filters):
|
|||||||
BOM_ITEM.stock_qty,
|
BOM_ITEM.stock_qty,
|
||||||
BOM_ITEM.stock_uom,
|
BOM_ITEM.stock_uom,
|
||||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||||
Sum(BIN.actual_qty).as_("actual_qty"),
|
BIN.actual_qty.as_("actual_qty"),
|
||||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||||
)
|
)
|
||||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
|||||||
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
|
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
|
||||||
erpnext.patches.v14_0.update_proprietorship_to_individual
|
erpnext.patches.v14_0.update_proprietorship_to_individual
|
||||||
erpnext.patches.v15_0.rename_subcontracting_fields
|
erpnext.patches.v15_0.rename_subcontracting_fields
|
||||||
|
erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage
|
||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import scrub
|
||||||
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.utils import flt
|
||||||
|
from semantic_version import Version
|
||||||
|
|
||||||
|
from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import (
|
||||||
|
AFFECTED_DOCTYPES,
|
||||||
|
LAST_MODIFIED_DATE_THRESHOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
# run this patch only if erpnext version before update is v15.64.0 or higher
|
||||||
|
version, git_branch = frappe.db.get_value(
|
||||||
|
"Installed Application",
|
||||||
|
{"app_name": "erpnext"},
|
||||||
|
["app_version", "git_branch"],
|
||||||
|
)
|
||||||
|
|
||||||
|
semantic_version = get_semantic_version(version)
|
||||||
|
if semantic_version and (
|
||||||
|
semantic_version.major < 15 or (git_branch == "version-15" and semantic_version.minor < 64)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
for doctype in AFFECTED_DOCTYPES:
|
||||||
|
meta = frappe.get_meta(doctype)
|
||||||
|
filters = {
|
||||||
|
"modified": [">", LAST_MODIFIED_DATE_THRESHOLD],
|
||||||
|
"additional_discount_percentage": [">", 0],
|
||||||
|
"discount_amount": ["!=", 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
# can't reverse calculate grand_total if shipping rule is set
|
||||||
|
if meta.has_field("shipping_rule"):
|
||||||
|
filters["shipping_rule"] = ["is", "not set"]
|
||||||
|
|
||||||
|
documents = frappe.get_all(
|
||||||
|
doctype,
|
||||||
|
fields=[
|
||||||
|
"name",
|
||||||
|
"additional_discount_percentage",
|
||||||
|
"discount_amount",
|
||||||
|
"apply_discount_on",
|
||||||
|
"grand_total",
|
||||||
|
"net_total",
|
||||||
|
],
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not documents:
|
||||||
|
continue
|
||||||
|
|
||||||
|
precision = get_field_precision(frappe.get_meta(doctype).get_field("additional_discount_percentage"))
|
||||||
|
mismatched_documents = []
|
||||||
|
|
||||||
|
for doc in documents:
|
||||||
|
# we need grand_total before applying discount
|
||||||
|
doc.grand_total += doc.discount_amount
|
||||||
|
discount_applied_on = scrub(doc.apply_discount_on)
|
||||||
|
calculated_discount_amount = flt(
|
||||||
|
doc.additional_discount_percentage * doc.get(discount_applied_on) / 100,
|
||||||
|
precision,
|
||||||
|
)
|
||||||
|
|
||||||
|
# if difference is more than 0.02 (based on precision), unset the additional discount percentage
|
||||||
|
if abs(calculated_discount_amount - doc.discount_amount) > 2 / (10**precision):
|
||||||
|
mismatched_documents.append(doc.name)
|
||||||
|
|
||||||
|
if mismatched_documents:
|
||||||
|
# changing the discount percentage has no accounting effect
|
||||||
|
# so we can safely set it to 0 in the database
|
||||||
|
frappe.db.set_value(
|
||||||
|
doctype,
|
||||||
|
{"name": ["in", mismatched_documents]},
|
||||||
|
"additional_discount_percentage",
|
||||||
|
0,
|
||||||
|
update_modified=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_semantic_version(version):
|
||||||
|
try:
|
||||||
|
return Version(version)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -44,23 +44,22 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
|
|
||||||
if (item.item_code && item.rate) {
|
if (item.item_code && item.rate) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "frappe.client.get_value",
|
method: "erpnext.stock.get_item_details.get_item_tax_template",
|
||||||
args: {
|
args: {
|
||||||
doctype: "Item Tax",
|
args: {
|
||||||
parent: "Item",
|
item_code: item.item_code,
|
||||||
filters: {
|
company: frm.doc.company,
|
||||||
parent: item.item_code,
|
base_net_rate: item.base_net_rate,
|
||||||
minimum_net_rate: ["<=", item.rate],
|
tax_category: frm.doc.tax_category,
|
||||||
maximum_net_rate: [">=", item.rate]
|
item_tax_template: item.item_tax_template,
|
||||||
},
|
posting_date: frm.doc.posting_date,
|
||||||
fieldname: "item_tax_template"
|
bill_date: frm.doc.bill_date,
|
||||||
|
transaction_date: frm.doc.transaction_date,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
const tax_rule = r.message;
|
const item_tax_template = r.message;
|
||||||
|
frappe.model.set_value(cdt, cdn, 'item_tax_template', item_tax_template);
|
||||||
let matched_template = tax_rule ? tax_rule.item_tax_template : null;
|
|
||||||
|
|
||||||
frappe.model.set_value(cdt, cdn, 'item_tax_template', matched_template);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ erpnext.timesheet.timer = function (frm, row, timestamp = 0) {
|
|||||||
{ fieldtype: "HTML", fieldname: "timer_html" },
|
{ fieldtype: "HTML", fieldname: "timer_html" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
dialog.set_values({
|
dialog.set_values({
|
||||||
activity_type: row.activity_type,
|
activity_type: row.activity_type,
|
||||||
@@ -26,6 +25,10 @@ erpnext.timesheet.timer = function (frm, row, timestamp = 0) {
|
|||||||
task: row.task,
|
task: row.task,
|
||||||
expected_hours: row.expected_hours,
|
expected_hours: row.expected_hours,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
dialog.set_values({
|
||||||
|
project: frm.doc.parent_project,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
dialog.get_field("timer_html").$wrapper.append(get_timer_html());
|
dialog.get_field("timer_html").$wrapper.append(get_timer_html());
|
||||||
function get_timer_html() {
|
function get_timer_html() {
|
||||||
|
|||||||
@@ -878,8 +878,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
fields: fields,
|
fields: fields,
|
||||||
primary_action: function () {
|
primary_action: function () {
|
||||||
var data = { items: d.fields_dict.items.grid.get_selected_children() };
|
var data = { items: d.fields_dict.items.grid.get_selected_children() };
|
||||||
if (!data) {
|
if (!data.items.length) {
|
||||||
frappe.throw(__("Please select items"));
|
frappe.throw(__("Please select atleast one item to continue"));
|
||||||
}
|
}
|
||||||
me.frm.call({
|
me.frm.call({
|
||||||
method: "make_work_orders",
|
method: "make_work_orders",
|
||||||
@@ -1029,7 +1029,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
items: data,
|
items: data,
|
||||||
company: me.frm.doc.company,
|
company: me.frm.doc.company,
|
||||||
sales_order: me.frm.docname,
|
sales_order: me.frm.docname,
|
||||||
project: me.frm.project,
|
project: me.frm.doc.project,
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
|
|||||||
@@ -1630,6 +1630,11 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
|||||||
|
|
||||||
items.update({"company": company, "sales_order": sales_order})
|
items.update({"company": company, "sales_order": sales_order})
|
||||||
|
|
||||||
|
item_wh = {}
|
||||||
|
for item in items.get("items"):
|
||||||
|
if item.get("warehouse"):
|
||||||
|
item_wh[item.get("item_code")] = item.get("warehouse")
|
||||||
|
|
||||||
raw_materials = get_items_for_material_requests(items)
|
raw_materials = get_items_for_material_requests(items)
|
||||||
if not raw_materials:
|
if not raw_materials:
|
||||||
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
||||||
@@ -1654,7 +1659,7 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
|||||||
"item_code": item.get("item_code"),
|
"item_code": item.get("item_code"),
|
||||||
"qty": item.get("quantity"),
|
"qty": item.get("quantity"),
|
||||||
"schedule_date": schedule_date,
|
"schedule_date": schedule_date,
|
||||||
"warehouse": item.get("warehouse"),
|
"warehouse": item_wh.get(item.get("main_bom_item")) or item.get("warehouse"),
|
||||||
"sales_order": sales_order,
|
"sales_order": sales_order,
|
||||||
"project": project,
|
"project": project,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
) {
|
) {
|
||||||
this.frm.doc.pos_profile = this.pos_profile;
|
this.frm.doc.pos_profile = this.pos_profile;
|
||||||
}
|
}
|
||||||
|
this.frm.doc.set_warehouse = this.settings.warehouse;
|
||||||
|
|
||||||
if (!this.frm.doc.company) return;
|
if (!this.frm.doc.company) return;
|
||||||
|
|
||||||
@@ -603,8 +604,6 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
|
|
||||||
async on_cart_update(args) {
|
async on_cart_update(args) {
|
||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
if (this.frm.doc.set_warehouse != this.settings.warehouse)
|
|
||||||
this.frm.doc.set_warehouse = this.settings.warehouse;
|
|
||||||
let item_row = undefined;
|
let item_row = undefined;
|
||||||
try {
|
try {
|
||||||
let { field, value, item } = args;
|
let { field, value, item } = args;
|
||||||
@@ -658,6 +657,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
new_item["use_serial_batch_fields"] = 1;
|
new_item["use_serial_batch_fields"] = 1;
|
||||||
|
new_item["warehouse"] = this.settings.warehouse;
|
||||||
if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0;
|
if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0;
|
||||||
|
|
||||||
item_row = this.frm.add_child("items", new_item);
|
item_row = this.frm.add_child("items", new_item);
|
||||||
|
|||||||
@@ -507,7 +507,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "personal_details",
|
"fieldname": "personal_details",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Personal"
|
"label": "Personal Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "passport_number",
|
"fieldname": "passport_number",
|
||||||
|
|||||||
@@ -1146,7 +1146,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
|||||||
|
|
||||||
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
|
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
|
||||||
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
||||||
elif item.billed_amt > amount:
|
elif amount and item.billed_amt > amount:
|
||||||
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
|
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:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
|
|||||||
@@ -163,8 +163,11 @@ class StockReconciliation(StockController):
|
|||||||
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
||||||
"""Set Serial and Batch Bundle for each item"""
|
"""Set Serial and Batch Bundle for each item"""
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not frappe.db.exists("Item", item.item_code):
|
if voucher_detail_no and voucher_detail_no != item.name:
|
||||||
frappe.throw(_("Item {0} does not exist").format(item.item_code))
|
continue
|
||||||
|
|
||||||
|
if not item.item_code:
|
||||||
|
continue
|
||||||
|
|
||||||
item_details = frappe.get_cached_value(
|
item_details = frappe.get_cached_value(
|
||||||
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||||
@@ -232,9 +235,6 @@ class StockReconciliation(StockController):
|
|||||||
if not save and item.use_serial_batch_fields:
|
if not save and item.use_serial_batch_fields:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if voucher_detail_no and voucher_detail_no != item.name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not item.current_serial_and_batch_bundle:
|
if not item.current_serial_and_batch_bundle:
|
||||||
serial_and_batch_bundle = frappe.get_doc(
|
serial_and_batch_bundle = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -286,6 +286,7 @@ class StockReconciliation(StockController):
|
|||||||
"warehouse": item.warehouse,
|
"warehouse": item.warehouse,
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
|
"for_stock_levels": True,
|
||||||
"ignore_voucher_nos": [self.name],
|
"ignore_voucher_nos": [self.name],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -887,6 +888,10 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
self.update_inventory_dimensions(row, data)
|
self.update_inventory_dimensions(row, data)
|
||||||
|
|
||||||
|
if self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle):
|
||||||
|
data.qty_after_transaction = data.actual_qty
|
||||||
|
data.actual_qty = 0.0
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def make_sle_on_cancel(self):
|
def make_sle_on_cancel(self):
|
||||||
|
|||||||
@@ -662,13 +662,17 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def get_item_tax_template(args, item, out):
|
@frappe.whitelist()
|
||||||
"""
|
def get_item_tax_template(args, item=None, out=None):
|
||||||
args = {
|
if isinstance(args, str):
|
||||||
"tax_category": None
|
args = json.loads(args)
|
||||||
"item_tax_template": None
|
|
||||||
}
|
if not item:
|
||||||
"""
|
if not args.get("item_code"):
|
||||||
|
frappe.throw(_("Item/Item Code required to get Item Tax Template."))
|
||||||
|
else:
|
||||||
|
item = frappe.get_cached_doc("Item", args.get("item_code"))
|
||||||
|
|
||||||
item_tax_template = None
|
item_tax_template = None
|
||||||
if item.taxes:
|
if item.taxes:
|
||||||
item_tax_template = _get_item_tax_template(args, item.taxes, out)
|
item_tax_template = _get_item_tax_template(args, item.taxes, out)
|
||||||
@@ -680,9 +684,11 @@ def get_item_tax_template(args, item, out):
|
|||||||
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
||||||
item_group = item_group_doc.parent_item_group
|
item_group = item_group_doc.parent_item_group
|
||||||
|
|
||||||
if args.get("child_doctype") and item_tax_template:
|
if out and args.get("child_doctype") and item_tax_template:
|
||||||
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
|
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
|
||||||
|
|
||||||
|
return item_tax_template
|
||||||
|
|
||||||
|
|
||||||
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||||
if out is None:
|
if out is None:
|
||||||
|
|||||||
@@ -159,10 +159,11 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None:
|
|||||||
|
|
||||||
def get_item_groups_map(svd_list: SVDList) -> dict[str, str]:
|
def get_item_groups_map(svd_list: SVDList) -> dict[str, str]:
|
||||||
item_codes = set(i["item_code"] for i in svd_list)
|
item_codes = set(i["item_code"] for i in svd_list)
|
||||||
ig_list = frappe.get_list(
|
return frappe._dict(
|
||||||
"Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)]
|
frappe.get_all(
|
||||||
|
"Item", fields=["name", "item_group"], filters=[("name", "in", item_codes)], as_list=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return {i["item_code"]: i["item_group"] for i in ig_list}
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_groups_dict() -> ItemGroupsDict:
|
def get_item_groups_dict() -> ItemGroupsDict:
|
||||||
|
|||||||
@@ -908,8 +908,11 @@ class update_entries_after:
|
|||||||
if not sle.is_adjustment_entry:
|
if not sle.is_adjustment_entry:
|
||||||
sle.stock_value_difference = stock_value_difference
|
sle.stock_value_difference = stock_value_difference
|
||||||
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
|
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
|
||||||
sle.stock_value_difference = get_stock_value_difference(
|
sle.stock_value_difference = (
|
||||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
get_stock_value_difference(
|
||||||
|
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||||
|
)
|
||||||
|
* -1
|
||||||
)
|
)
|
||||||
|
|
||||||
sle.doctype = "Stock Ledger Entry"
|
sle.doctype = "Stock Ledger Entry"
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ def get_stock_balance(
|
|||||||
|
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
|
|
||||||
|
frappe.has_permission("Item", "read", throw=True)
|
||||||
|
|
||||||
if posting_date is None:
|
if posting_date is None:
|
||||||
posting_date = nowdate()
|
posting_date = nowdate()
|
||||||
if posting_time is None:
|
if posting_time is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user