Merge pull request #54741 from frappe/version-15-hotfix

This commit is contained in:
diptanilsaha
2026-05-05 22:00:21 +05:30
committed by GitHub
29 changed files with 638 additions and 437 deletions

View File

@@ -34,6 +34,13 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -317,13 +324,21 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Stock Adjustment"
"account_type": "Expense Account"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -340,7 +355,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_type": "Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -1,108 +1,41 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-11-22 23:47:02.804568",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2018-11-22 23:47:02.804568",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"tax_type",
"tax_rate"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "tax_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax",
"options": "Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_rate",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-12-21 23:51:39.445198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"istable": 1,
"links": [],
"modified": "2026-04-30 23:49:27.020639",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -2328,16 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += f" and {fieldname} between {frappe.db.escape(args.get(date_fields[0]))} and {frappe.db.escape(args.get(date_fields[1]))}"
condition += f" and {fieldname} between {from_date} and {to_date}"
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= {frappe.db.escape(args.get(date_fields[0]))}"
condition += f" and {fieldname} >= {from_date}"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= {frappe.db.escape(args.get(date_fields[1]))}"
condition += f" and {fieldname} <= {to_date}"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):

View File

@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -126,7 +131,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -460,13 +471,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
on_submit() {
@@ -575,12 +587,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -136,13 +136,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
cur_frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
},
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
}
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -540,12 +542,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
set_dynamic_labels() {

View File

@@ -440,7 +440,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
if (
frappe.model.can_create("Payment Entry") &&
flt(doc.per_billed) < 100 &&
doc.status != "Delivered"
) {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -448,7 +452,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed) < 100) {
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -705,12 +709,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (doc.schedule_date) {
row.schedule_date = doc.schedule_date;
refresh_field("schedule_date", cdn, "items");
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
field_copy.push("project");
}
if (doc.schedule_date) {
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
} else {
field_copy.push("schedule_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
@@ -785,12 +797,6 @@ cur_frm.cscript.update_status = function (label, status) {
});
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -115,9 +115,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
// for backward compatibility: combine new and previous states
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};

View File

@@ -364,13 +364,18 @@ class Project(Document):
)
for user in self.users:
# process only users who haven't received the welcome email yet
if user.welcome_email_sent == 0:
frappe.sendmail(
user.user,
subject=_("Project Collaboration Invitation"),
content=content,
)
user.welcome_email_sent = 1
# fetch canonical User data (enabled status + latest email)
user_info = frappe.db.get_value("User", user.user, ["enabled", "email"], as_dict=True)
# send email only if user is enabled and has a valid email
if user_info and user_info.enabled and user_info.email:
frappe.sendmail(
recipients=[user_info.email],
subject=_("Project Collaboration Invitation"),
content=content,
)
user.welcome_email_sent = 1
def get_timeline_data(doctype: str, name: str) -> dict[int, int]:

View File

@@ -25,13 +25,15 @@ erpnext.buying = {
};
});
this.frm.set_query("project", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
const project_filters = {
query: "erpnext.controllers.queries.get_project_name",
filters: {
company: doc.company,
},
};
this.frm.set_query("project", (_) => project_filters);
this.frm.set_query("project", "items", (_, __, ___) => project_filters);
if (this.frm.doc.__islocal
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {

View File

@@ -560,7 +560,9 @@ def _make_customer(source_name, ignore_permissions=False):
if quotation.quotation_to == "Customer":
return frappe.get_doc("Customer", quotation.party_name)
elif quotation.quotation_to == "CRM Deal":
return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name})
if customer_name:
return frappe.get_doc("Customer", customer_name)
# Check if a Customer already exists for the Lead or Prospect.
existing_customer = None

View File

@@ -175,6 +175,61 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(quotation.payment_schedule)
def test_terms_attachments_are_copied_to_quotation(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
first_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="First terms attachment",
)
quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()
self.assertEqual(get_attachment_urls("Quotation", quotation.name), {first_attachment.file_url})
second_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="Second terms attachment",
)
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()
quotation_attachments = get_attachment_urls("Quotation", quotation.name)
self.assertEqual(quotation_attachments, {first_attachment.file_url})
self.assertNotIn(second_attachment.file_url, quotation_attachments)
new_terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
new_terms_attachment = make_file_attachment(
"Terms and Conditions",
new_terms.name,
content="Attachment from updated terms",
)
quotation.tc_name = new_terms.name
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()
self.assertEqual(
get_attachment_urls("Quotation", quotation.name),
{first_attachment.file_url, new_terms_attachment.file_url},
)
def test_terms_attachments_are_not_copied_when_disabled(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=False)
make_file_attachment(
"Terms and Conditions",
terms.name,
content="Terms attachment should stay on the template",
)
quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()
self.assertFalse(get_attachment_urls("Quotation", quotation.name))
@change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
@@ -1142,6 +1197,42 @@ def get_quotation_dict(party_name=None, item_code=None):
}
def make_terms_and_conditions(copy_attachments_to_transaction=False):
return frappe.get_doc(
{
"doctype": "Terms and Conditions",
"title": f"_Test Terms and Conditions {frappe.generate_hash(length=8)}",
"selling": 1,
"terms": "Test terms",
"copy_attachments_to_transaction": 1 if copy_attachments_to_transaction else 0,
}
).insert()
def make_file_attachment(doctype, docname, content):
return frappe.get_doc(
{
"doctype": "File",
"file_name": f"terms-attachment-{frappe.generate_hash(length=8)}.txt",
"attached_to_doctype": doctype,
"attached_to_name": docname,
"content": content,
}
).insert()
def get_attachment_urls(doctype, docname):
return {
file.file_url
for file in frappe.get_all(
"File",
filters={"attached_to_doctype": doctype, "attached_to_name": docname},
fields=["file_url"],
)
if file.file_url
}
def make_quotation(**args):
qo = frappe.new_doc("Quotation")
args = frappe._dict(args)

View File

@@ -773,11 +773,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// payment request
if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request(),
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request_with_schedule(),
__("Create")
);
}
if (frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(
@@ -833,6 +835,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
this.order_type(doc);
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
if (doc.delivery_date) {
frappe.model.set_value(cdt, cdn, "delivery_date", doc.delivery_date);
} else {
field_copy.push("delivery_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
create_pick_list() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",

View File

@@ -533,6 +533,7 @@ class SalesOrder(SellingController):
self.update_reserved_qty()
self.notify_update()
clear_doctype_notifications(self)
self.update_blanket_order()
def update_reserved_qty(self, so_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""

View File

@@ -1,122 +1,176 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, msgprint
from frappe import _
from frappe.query_builder import DocType, Field, Order
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.utils import QueryBuilder
from frappe.utils.data import comma_or
SALES_TRANSACTION_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note", "POS Invoice"]
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
return SalesPartnerCommissionSummaryReport(filters).run()
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
class SalesPartnerSummaryReport:
"""
Base class to generate Sales Partner Summary related Reports.
"""
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Total Commission"),
"fieldname": "total_commission",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
]
dt: DocType
date_field: str
date_label: str
columns: list
data: list
query: QueryBuilder
filters: dict
return columns
def __init__(self, filters: dict):
self.filters = filters
self.columns = []
def run(self):
self.validate_filters()
self.prepare_columns()
self.get_data()
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
return self.columns, self.data
def validate_filters(self):
if not self.filters.get("doctype"):
frappe.throw(_("Please select the document type first."))
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
if not self.filters.get("company"):
frappe.throw(_("Please select a company."))
if (
self.filters.get("from_date")
and self.filters.get("to_date")
and self.filters.get("from_date") > self.filters.get("to_date")
):
frappe.throw(_("From Date cannot be greater than To Date."))
self._set_date_field_and_label()
def _set_date_field_and_label(self):
self.date_field = (
"transaction_date" if self.filters.get("doctype") == "Sales Order" else "posting_date"
)
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
def prepare_columns(self):
"""
SELECT
name, customer, territory, {} as posting_date, base_net_total as amount,
sales_partner, commission_rate, total_commission, '{}' as currency
FROM
`tab{}`
WHERE
{} and docstatus = 1 and sales_partner is not null
and sales_partner != '' order by name desc, sales_partner
""".format(date_field, company_currency, filters.get("doctype"), conditions),
filters,
as_dict=1,
)
Extend this method to add columns on the report. Use `make_column` to add more columns.
"""
raise NotImplementedError
return entries
def get_data(self):
self.build_report_query()
self.data = self.query.run(as_dict=1)
def build_report_query(self):
self._build_report_base_query()
self.extend_report_query()
self._apply_common_filters()
self.apply_filters()
def _build_report_base_query(self):
self.dt = DocType(self.filters.get("doctype"))
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
self.query = (
frappe.qb.from_(self.dt)
.select(
self.dt.name,
self.dt.customer,
self.dt.territory,
Field(self.date_field, "posting_date", table=self.dt),
self.dt.sales_partner,
self.dt.commission_rate,
ConstantColumn(company_currency).as_("currency"),
)
.where(
(self.dt.docstatus == 1) & (self.dt.sales_partner.notnull()) & (self.dt.sales_partner != "")
)
.orderby(self.dt.name, order=Order.desc)
.orderby(self.dt.sales_partner)
)
def extend_report_query(self):
"""
Extend this method to select more columns on the query.
"""
pass
def _apply_common_filters(self):
for field in ["company", "customer", "territory", "sales_partner"]:
if self.filters.get(field):
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
if self.filters.get("from_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
)
if self.filters.get("to_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
)
def apply_filters(self):
"""
Extend this method to add more conditions on the query.
"""
pass
def make_column(
self, label: str, fieldname: str, fieldtype: str, width: int = 140, options: str = "", hidden: int = 0
):
self.columns.append(
dict(
label=label,
fieldname=fieldname,
fieldtype=fieldtype,
options=options,
width=width,
hidden=hidden,
)
)
def get_conditions(filters, date_field):
conditions = "1=1"
class SalesPartnerCommissionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
for field in ["company", "customer", "territory"]:
if filters.get(field):
conditions += f" and {field} = %({field})s"
self.make_column(_("Customer"), "customer", "Link", options="Customer")
if filters.get("sales_partner"):
conditions += " and sales_partner = %(sales_partner)s"
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
if filters.get("from_date"):
conditions += f" and {date_field} >= %(from_date)s"
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
if filters.get("to_date"):
conditions += f" and {date_field} <= %(to_date)s"
self.make_column(self.date_label, "posting_date", "Date")
return conditions
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
def extend_report_query(self):
self.query = self.query.select(
self.dt.base_net_total.as_("amount"),
self.dt.total_commission,
)

View File

@@ -3,6 +3,14 @@
frappe.query_reports["Sales Partner Transaction Summary"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "sales_partner",
label: __("Sales Partner"),
@@ -28,14 +36,6 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "item_group",
label: __("Item Group"),

View File

@@ -3,144 +3,84 @@
import frappe
from frappe import _, msgprint
from frappe import _
from frappe.query_builder import Case
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
SalesPartnerSummaryReport,
)
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
return SalesPartnerTransactionSummaryReport(filters=filters).run()
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
class SalesPartnerTransactionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 100,
},
{
"label": _("Item Group"),
"fieldname": "item_group",
"fieldtype": "Link",
"options": "Item Group",
"width": 100,
},
{
"label": _("Brand"),
"fieldname": "brand",
"fieldtype": "Link",
"options": "Brand",
"width": 100,
},
{"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
{"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120},
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"width": 120,
},
]
self.make_column(_("Customer"), "customer", "Link", options="Customer")
return columns
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
self.make_column(self.date_label, "posting_date", "Date")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
SELECT
dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency,
dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount,
((dt_item.base_net_amount * dt.commission_rate) / 100) as commission,
dt_item.brand, dt.sales_partner, dt.commission_rate, dt_item.item_group, dt_item.item_code
FROM
`tab{doctype}` dt, `tab{doctype} Item` dt_item
WHERE
{cond} and dt.name = dt_item.parent and dt.docstatus = 1
and dt.sales_partner is not null and dt.sales_partner != ''
order by dt.name desc, dt.sales_partner
""".format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions),
filters,
as_dict=1,
)
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
return entries
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
def get_conditions(filters, date_field):
conditions = "1=1"
self.make_column(_("Quantity"), "qty", "Float", 120)
for field in ["company", "customer", "territory", "sales_partner"]:
if filters.get(field):
conditions += f" and dt.{field} = %({field})s"
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
if filters.get("from_date"):
conditions += f" and dt.{date_field} >= %(from_date)s"
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
if filters.get("to_date"):
conditions += f" and dt.{date_field} <= %(to_date)s"
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
if not filters.get("show_return_entries"):
conditions += " and dt_item.qty > 0.0"
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
if filters.get("brand"):
conditions += " and dt_item.brand = %(brand)s"
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
if filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"])
def extend_report_query(self):
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
conditions += f""" and dt_item.item_group in (select name from
`tabItem Group` where lft >= {lft} and rgt <= {rgt})"""
self.query = (
self.query.join(self.dt_item)
.on(self.dt.name == self.dt_item.parent)
.select(
self.dt_item.base_net_rate.as_("rate"),
self.dt_item.qty,
self.dt_item.base_net_amount.as_("amount"),
Case()
.when(
self.dt_item.grant_commission.eq(1),
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
)
.else_(0)
.as_("commission"),
self.dt_item.brand,
self.dt_item.item_group,
self.dt_item.item_code,
)
)
return conditions
def apply_filters(self):
if not self.filters.get("show_return_entries"):
self.query = self.query.where(self.dt_item.qty > 0.0)
if self.filters.get("brand"):
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
if self.filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
if item_groups := frappe.get_all(
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
):
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))

View File

@@ -11,6 +11,8 @@
"field_order": [
"title",
"disabled",
"column_break_ofhb",
"copy_attachments_to_transaction",
"applicable_modules_section",
"selling",
"buying",
@@ -72,12 +74,22 @@
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ofhb",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "copy_attachments_to_transaction",
"fieldtype": "Check",
"label": "Copy Attachments to Transaction"
}
],
"icon": "icon-legal",
"idx": 1,
"links": [],
"modified": "2024-01-30 12:47:52.325531",
"modified": "2026-04-29 22:51:49.285298",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",

View File

@@ -21,6 +21,7 @@ class TermsandConditions(Document):
from frappe.types import DF
buying: DF.Check
copy_attachments_to_transaction: DF.Check
disabled: DF.Check
selling: DF.Check
terms: DF.TextEditor | None

View File

@@ -372,6 +372,15 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
});
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["project"]);
}
}
make_sales_invoice() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",

View File

@@ -368,11 +368,13 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"cost_center",
"project",
]);
const field_copy = ["expense_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
};

View File

@@ -673,6 +673,9 @@ class PurchaseReceipt(BuyingController):
or stock_asset_rbnb
)
if self.is_return and item.expense_account:
loss_account = item.expense_account
cost_center = item.cost_center or frappe.get_cached_value(
"Company", self.company, "cost_center"
)

View File

@@ -1042,7 +1042,7 @@
"search_index": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
@@ -1057,7 +1057,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -1148,7 +1148,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-04-07 15:41:47.032889",
"modified": "2026-04-29 16:01:34.154697",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -392,10 +392,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
f"""
SELECT distinct item_code, item_name
SELECT distinct `tab{from_doctype}`.item_code, `tab{from_doctype}`.item_name
FROM `tab{from_doctype}`
JOIN `tab{parent_doctype}` ON `tab{parent_doctype}`.name = `tab{from_doctype}`.parent
WHERE parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and item_code like %(txt)s
WHERE `tab{from_doctype}`.parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and `tab{from_doctype}`.item_code like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
""",

View File

@@ -356,8 +356,15 @@ def repost(doc):
message = message.get("message")
status = "Failed"
# If failed because of timeout, set status to In Progress
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
# If failed because of a recoverable error (timeout, deadlock), set status to In Progress
# so the scheduler automatically retries instead of leaving it permanently failed.
# NOTE: isinstance check comes first because the traceback string matching is unreliable
# when SIGALRM kills the process mid-C-extension (JobTimeoutException may not appear
# in the traceback if the exception handler itself was interrupted).
traceback_lower = traceback.lower() if traceback else ""
if isinstance(e, RecoverableErrors) or (
traceback_lower and ("timeout" in traceback_lower or "deadlock found" in traceback_lower)
):
status = "In Progress"
if traceback:

View File

@@ -1031,6 +1031,7 @@ def insert_item_price(args):
currency=args.currency,
uom=args.stock_uom,
price_list=args.price_list,
valid_from=transaction_date,
)
item_price.insert()
frappe.msgprint(
@@ -1055,6 +1056,7 @@ def insert_item_price(args):
"currency": args.currency,
"price_list_rate": price_list_rate,
"uom": args.stock_uom,
"valid_from": transaction_date,
}
)
item_price.insert()

View File

@@ -7,6 +7,7 @@ import frappe
from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
from erpnext.stock.serial_batch_bundle import get_serial_no_status
from erpnext.stock.stock_ledger import get_stock_ledger_entries
BUYING_VOUCHER_TYPES = ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]
@@ -111,7 +112,7 @@ def get_data(filters):
"posting_time": row.posting_time,
"voucher_type": row.voucher_type,
"voucher_no": row.voucher_no,
"status": "Active" if row.actual_qty > 0 else "Delivered",
"status": get_serial_no_status(row),
"company": row.company,
"warehouse": row.warehouse,
"qty": 1 if row.actual_qty > 0 else -1,

View File

@@ -73,6 +73,7 @@ def execute(filters=None):
inv_dimension_wise_dict, filters, inv_dimension_key=inv_dimension_key, opening_row=opening_row
)
item_wh_wise_prev_sle = {}
for sle in sl_entries:
item_detail = item_details[sle.item_code]
@@ -114,6 +115,21 @@ def execute(filters=None):
elif sle.voucher_type == "Stock Reconciliation":
sle["in_out_rate"] = sle.valuation_rate
if (
sle.voucher_type == "Stock Reconciliation"
and not sle.in_qty
and not sle.out_qty
and not sle.actual_qty
):
if prev_sle := item_wh_wise_prev_sle.get((sle.item_code, sle.warehouse)):
bal_qty = prev_sle.get("qty_after_transaction", 0)
qty = sle.qty_after_transaction - bal_qty
if qty > 0:
sle.in_qty = qty
elif qty < 0:
sle.out_qty = qty
item_wh_wise_prev_sle[(sle.item_code, sle.warehouse)] = sle
data.append(sle)
if include_uom:

View File

@@ -15,6 +15,45 @@ from erpnext.stock.deprecated_serial_batch import (
)
from erpnext.stock.valuation import round_off_if_near_zero
CONSUMED_SERIAL_NO_STOCK_ENTRY_PURPOSES = (
"Manufacture",
"Material Issue",
"Repack",
"Material Consumption for Manufacture",
)
INACTIVE_SERIAL_NO_STOCK_ENTRY_PURPOSES = ("Disassemble", "Material Receipt")
def get_serial_no_status(sle):
warehouse = sle.warehouse if sle.actual_qty > 0 else None
if warehouse:
return "Active"
status = get_status_for_serial_nos(sle)
if sle.voucher_type == "Stock Entry" and sle.actual_qty < 0:
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in INACTIVE_SERIAL_NO_STOCK_ENTRY_PURPOSES:
status = "Inactive"
return status
def get_status_for_serial_nos(sle):
status = "Inactive"
if sle.actual_qty < 0:
status = "Delivered"
if sle.voucher_type == "Stock Entry":
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in CONSUMED_SERIAL_NO_STOCK_ENTRY_PURPOSES:
status = "Consumed"
if sle.is_cancelled == 1 and (
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
):
status = "Inactive"
return status
class SerialBatchBundle:
def __init__(self, **kwargs):
@@ -410,25 +449,7 @@ class SerialBatchBundle:
self.update_serial_no_status_warehouse(self.sle, serial_nos)
def get_status_for_serial_nos(self, sle):
status = "Inactive"
if sle.actual_qty < 0:
status = "Delivered"
if sle.voucher_type == "Stock Entry":
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in [
"Manufacture",
"Material Issue",
"Repack",
"Material Consumption for Manufacture",
]:
status = "Consumed"
if sle.is_cancelled == 1 and (
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
):
status = "Inactive"
return status
return get_status_for_serial_nos(sle)
def update_serial_no_status_warehouse(self, sle, serial_nos):
warehouse = sle.warehouse if sle.actual_qty > 0 else None
@@ -436,19 +457,12 @@ class SerialBatchBundle:
if isinstance(serial_nos, str):
serial_nos = [serial_nos]
status = "Active"
if not warehouse:
status = self.get_status_for_serial_nos(sle)
status = get_serial_no_status(sle)
customer = None
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 = (

View File

@@ -15,6 +15,14 @@ class UOMMustBeIntegerError(frappe.ValidationError):
class TransactionBase(StatusUpdater):
def on_change(self):
# `on_change` also fires for `db_set()`, so only run during an actual insert/save.
is_real_save = self.flags.in_insert or (self.doctype, self.name) in frappe.flags.currently_saving
if not is_real_save:
return
self.copy_terms_and_conditions_attachments()
def validate_posting_time(self):
# set Edit Posting Date and Time to 1 while data import
if frappe.flags.in_import and self.posting_date:
@@ -33,6 +41,56 @@ class TransactionBase(StatusUpdater):
def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
validate_uom_is_integer(self, uom_field, qty_fields, child_dt)
def copy_terms_and_conditions_attachments(self):
if (
not self.name
or not self.meta.has_field("tc_name")
or not self.tc_name
or not self.has_value_changed("tc_name")
):
return
copy_attachments_to_transaction = frappe.db.get_value(
"Terms and Conditions", self.tc_name, "copy_attachments_to_transaction"
)
if not cint(copy_attachments_to_transaction):
return
source_attachments = frappe.get_all(
"File",
filters={
"attached_to_doctype": "Terms and Conditions",
"attached_to_name": self.tc_name,
},
fields=["name", "file_url"],
)
if not source_attachments:
return
existing_file_urls = {
attachment.file_url
for attachment in frappe.get_all(
"File",
filters={
"attached_to_doctype": self.doctype,
"attached_to_name": self.name,
},
fields=["file_url"],
)
if attachment.file_url
}
for source_attachment in source_attachments:
if not source_attachment.file_url or source_attachment.file_url in existing_file_urls:
continue
# Reuse the existing file metadata so the same on-disk blob is shared.
new_attachment = frappe.get_doc("File", source_attachment.name).create_attachment_copy(
attached_to_doctype=self.doctype,
attached_to_name=self.name,
)
existing_file_urls.add(new_attachment.file_url)
def validate_with_previous_doc(self, ref):
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []