diff --git a/frappe/www/attribution.html b/frappe/www/attribution.html
new file mode 100644
index 0000000000..efeed6024c
--- /dev/null
+++ b/frappe/www/attribution.html
@@ -0,0 +1,42 @@
+{% extends "templates/web.html" %}
+
+{% block page_content %}
+
+
{{ _("Attribution") }}
+
+ {{ _("This software is built on top of many open source packages. We would like to thank the authors of these packages for their contribution.") }}
+
+
+{% for app, packages in packages_by_app.items() %}
+
+
+
+
+
+ | {{ _("Package") }} |
+ {{ _("Version") }} |
+ {{ _("License") }} |
+ {{ _("Author") }} |
+
+
+
+ {% for package in packages %}
+
+ |
+ {% if package.homepage %}
+ {{ package.name | e }}
+ {% else %}
+ {{ package.name | e }}
+ {% endif %}
+ |
+ {{ package.version | e }} |
+ {{ (package.license | e) or _("Unknown") }} |
+ {{ (package.author | e) or _("Unknown") }} |
+
+ {% endfor %}
+
+
+
+{% endfor %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/frappe/www/attribution.py b/frappe/www/attribution.py
new file mode 100644
index 0000000000..bfc8f2eda7
--- /dev/null
+++ b/frappe/www/attribution.py
@@ -0,0 +1,78 @@
+import json
+import re
+import tomllib
+
+import requests
+
+import frappe
+
+
+def get_context(context):
+ packages_by_app = {}
+ for app in frappe.get_installed_apps():
+ packages_by_app[app] = get_app_deps(app)
+
+ context.packages_by_app = packages_by_app
+
+
+def get_app_deps(app: str):
+ dependencies = []
+
+ app_info = get_pyproject_info(app)
+ for requirement in app_info.get("dependencies", []):
+ name, version = parse_pip_requirement(requirement)
+ info = get_py_registry_info(name)
+ info["name"] = name
+ info["version"] = version
+ dependencies.append(info)
+
+ for name, version in get_js_deps(app).items():
+ info = get_js_registry_info(name)
+ info["name"] = name
+ info["version"] = version
+ dependencies.append(info)
+
+ return dependencies
+
+
+def get_js_deps(app: str):
+ package_json = frappe.get_app_path(app, "..", "package.json")
+ with open(package_json) as f:
+ package = json.load(f)
+
+ return package.get("dependencies", {})
+
+
+def get_js_registry_info(package):
+ registry_url = f"https://registry.npmjs.org/{package}"
+ registry_info = requests.get(registry_url).json()
+ return {
+ "license": registry_info.get("license"),
+ "author": registry_info.get("author", {}).get("name"),
+ "homepage": registry_info.get("homepage"),
+ }
+
+
+def get_pyproject_info(app: str) -> list[str]:
+ pyproject_toml = frappe.get_app_path(app, "..", "pyproject.toml")
+ with open(pyproject_toml, "rb") as f:
+ pyproject = tomllib.load(f)
+
+ return pyproject.get("project", {})
+
+
+def get_py_registry_info(package):
+ registry_url = f"https://pypi.org/pypi/{package}/json"
+ registry_info = requests.get(registry_url).json().get("info", {})
+ return {
+ "license": registry_info.get("license"),
+ "author": registry_info.get("author"),
+ "homepage": registry_info.get("home_page"),
+ }
+
+
+def parse_pip_requirement(requirement: str) -> tuple[str, str]:
+ """Parse pip requirement string to package name and version"""
+ match = re.match(r"^([A-Za-z0-9_\-\[\]]+)(.*)$", requirement)
+
+ return (match[1], match[2]) if match else (requirement, "")