Loading...
Loading...
Compare original and translation side by side
/frappe-app <app_name> [--module <module_name>]/frappe-app inventory_management
/frappe-app hr_extension --module Human Resources/frappe-app <app_name> [--module <module_name>]/frappe-app inventory_management
/frappe-app hr_extension --module Human Resourcesinventory_profrappe-bench/appspwd
ls -lainventory_profrappe-bench/appspwd
ls -la<app_name>/
├── <app_name>/
│ ├── __init__.py
│ ├── hooks.py # App hooks and integrations
│ ├── modules.txt # Module definitions
│ ├── patches.txt # Database migrations
│ ├── <module_name>/ # Primary module
│ │ ├── __init__.py
│ │ ├── doctype/ # DocType definitions
│ │ │ └── __init__.py
│ │ ├── api/ # REST API endpoints (v2)
│ │ │ └── __init__.py
│ │ ├── services/ # Business logic layer
│ │ │ └── __init__.py
│ │ ├── repositories/ # Data access layer
│ │ │ └── __init__.py
│ │ └── report/ # Custom reports
│ │ └── __init__.py
│ ├── public/
│ │ ├── css/
│ │ └── js/
│ ├── templates/
│ │ ├── includes/
│ │ └── pages/
│ ├── www/ # Portal pages
│ └── tests/
│ ├── __init__.py
│ └── test_utils.py
├── pyproject.toml
├── README.md
└── license.txt<app_name>/
├── <app_name>/
│ ├── __init__.py
│ ├── hooks.py # App hooks and integrations
│ ├── modules.txt # Module definitions
│ ├── patches.txt # Database migrations
│ ├── <module_name>/ # Primary module
│ │ ├── __init__.py
│ │ ├── doctype/ # DocType definitions
│ │ │ └── __init__.py
│ │ ├── api/ # REST API endpoints (v2)
│ │ │ └── __init__.py
│ │ ├── services/ # Business logic layer
│ │ │ └── __init__.py
│ │ ├── repositories/ # Data access layer
│ │ │ └── __init__.py
│ │ └── report/ # Custom reports
│ │ └── __init__.py
│ ├── public/
│ │ ├── css/
│ │ └── js/
│ ├── templates/
│ │ ├── includes/
│ │ └── pages/
│ ├── www/ # Portal pages
│ └── tests/
│ ├── __init__.py
│ └── test_utils.py
├── pyproject.toml
├── README.md
└── license.txt[project]
name = "<app_name>"
version = "0.0.1"
description = "<description>"
authors = [
{name = "<author>", email = "<email>"}
]
requires-python = ">=3.10"
readme = "README.md"
license = {text = "MIT"}
dependencies = [
"frappe>=15.0.0"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
]
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
[tool.pytest.ini_options]
testpaths = ["<app_name>/tests"]
python_files = "test_*.py"[project]
name = "<app_name>"
version = "0.0.1"
description = "<description>"
authors = [
{name = "<author>", email = "<email>"}
]
requires-python = ">=3.10"
readme = "README.md"
license = {text = "MIT"}
dependencies = [
"frappe>=15.0.0"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
]
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
[tool.pytest.ini_options]
testpaths = ["<app_name>/tests"]
python_files = "test_*.py"app_name = "<app_name>"
app_title = "<App Title>"
app_publisher = "<Author>"
app_description = "<Description>"
app_email = "<email>"
app_license = "MIT"app_name = "<app_name>"
app_title = "<App Title>"
app_publisher = "<Author>"
app_description = "<Description>"
app_email = "<email>"
app_license = "MIT"undefinedundefined<app_name>/<module>/services/base.py"""
Base service class providing common functionality for all services.
Services contain business logic and orchestrate operations.
"""
import frappe
from frappe import _
from typing import TYPE_CHECKING, Optional, Any
if TYPE_CHECKING:
from frappe.model.document import Document
class BaseService:
"""
Base class for all service layer classes.
Services should:
- Contain business logic
- Coordinate between repositories
- Handle transactions
- Validate business rules
"""
def __init__(self, user: Optional[str] = None):
self.user = user or frappe.session.user
def check_permission(
self,
doctype: str,
ptype: str = "read",
doc: Optional["Document"] = None,
throw: bool = True
) -> bool:
"""Check if current user has permission."""
return frappe.has_permission(
doctype=doctype,
ptype=ptype,
doc=doc,
user=self.user,
throw=throw
)
def validate_mandatory(self, data: dict, fields: list[str]) -> None:
"""Validate that mandatory fields are present."""
missing = [f for f in fields if not data.get(f)]
if missing:
frappe.throw(
_("Missing required fields: {0}").format(", ".join(missing))
)
def log_activity(
self,
doctype: str,
docname: str,
action: str,
details: Optional[dict] = None
) -> None:
"""Log service activity for audit trail."""
frappe.get_doc({
"doctype": "Comment",
"comment_type": "Info",
"reference_doctype": doctype,
"reference_name": docname,
"content": f"{action}: {details}" if details else action
}).insert(ignore_permissions=True)<app_name>/<module>/services/base.py"""
Base service class providing common functionality for all services.
Services contain business logic and orchestrate operations.
"""
import frappe
from frappe import _
from typing import TYPE_CHECKING, Optional, Any
if TYPE_CHECKING:
from frappe.model.document import Document
class BaseService:
"""
Base class for all service layer classes.
Services should:
- Contain business logic
- Coordinate between repositories
- Handle transactions
- Validate business rules
"""
def __init__(self, user: Optional[str] = None):
self.user = user or frappe.session.user
def check_permission(
self,
doctype: str,
ptype: str = "read",
doc: Optional["Document"] = None,
throw: bool = True
) -> bool:
"""Check if current user has permission."""
return frappe.has_permission(
doctype=doctype,
ptype=ptype,
doc=doc,
user=self.user,
throw=throw
)
def validate_mandatory(self, data: dict, fields: list[str]) -> None:
"""Validate that mandatory fields are present."""
missing = [f for f in fields if not data.get(f)]
if missing:
frappe.throw(
_("Missing required fields: {0}").format(", ".join(missing))
)
def log_activity(
self,
doctype: str,
docname: str,
action: str,
details: Optional[dict] = None
) -> None:
"""Log service activity for audit trail."""
frappe.get_doc({
"doctype": "Comment",
"comment_type": "Info",
"reference_doctype": doctype,
"reference_name": docname,
"content": f"{action}: {details}" if details else action
}).insert(ignore_permissions=True)<app_name>/<module>/repositories/base.py"""
Base repository class for data access operations.
Repositories handle all database interactions.
"""
import frappe
from frappe.query_builder import DocType
from typing import TYPE_CHECKING, Optional, Any, TypeVar, Generic
if TYPE_CHECKING:
from frappe.model.document import Document
T = TypeVar("T", bound="Document")
class BaseRepository(Generic[T]):
"""
Base class for all repository layer classes.
Repositories should:
- Handle all database operations
- Provide clean data access interface
- Abstract SQL/ORM details
- Never contain business logic
Performance Notes:
- Use get_cached() for repeated reads of same document
- Use get_value() when you only need 1-2 fields (faster than get_doc)
- get_list() applies user permissions; use get_all() to bypass (internal use only)
"""
doctype: str = ""
def __init__(self):
if not self.doctype:
raise ValueError("Repository must define doctype attribute")
def get(self, name: str, for_update: bool = False) -> Optional[T]:
"""
Get document by name. Fetches ALL fields and child tables.
For better performance when reading 1-2 fields, use get_value() instead.
For repeated reads of same document, use get_cached() instead.
"""
if not frappe.db.exists(self.doctype, name):
return None
return frappe.get_doc(self.doctype, name, for_update=for_update)
def get_cached(self, name: str) -> Optional[T]:
"""
Get document with caching. Use for repeated reads within same request.
Returns cached version if available, otherwise fetches and caches.
Cache is automatically invalidated when document is saved.
Can provide 10000x+ performance improvement for repeated reads.
"""
if not frappe.db.exists(self.doctype, name):
return None
return frappe.get_cached_doc(self.doctype, name)
def get_or_throw(self, name: str, for_update: bool = False) -> T:
"""Get document by name or throw if not found."""
doc = self.get(name, for_update=for_update)
if not doc:
frappe.throw(f"{self.doctype} {name} not found")
return doc
def exists(self, name: str) -> bool:
"""Check if document exists."""
return frappe.db.exists(self.doctype, name)
def get_list(
self,
filters: Optional[dict] = None,
fields: Optional[list[str]] = None,
order_by: str = "modified desc",
limit: int = 20,
offset: int = 0
) -> list[dict]:
"""
Get list of documents with user permission filtering.
Note: This applies user permissions automatically.
For internal/admin queries without permission checks, use get_all().
"""
return frappe.get_list(
self.doctype,
filters=filters,
fields=fields or ["name"],
order_by=order_by,
limit_page_length=limit,
limit_start=offset
)
def get_all(
self,
filters: Optional[dict] = None,
fields: Optional[list[str]] = None,
order_by: str = "modified desc",
limit: int = 20,
offset: int = 0
) -> list[dict]:
"""
Get list of documents WITHOUT permission filtering.
WARNING: Use only for internal/system operations.
For user-facing queries, use get_list() instead.
"""
return frappe.get_all(
self.doctype,
filters=filters,
fields=fields or ["name"],
order_by=order_by,
limit_page_length=limit,
limit_start=offset
)
def get_count(self, filters: Optional[dict] = None) -> int:
"""Get count of documents matching filters."""
return frappe.db.count(self.doctype, filters=filters)
def create(self, data: dict) -> T:
"""Create new document."""
doc = frappe.get_doc({"doctype": self.doctype, **data})
doc.insert()
return doc
def update(self, name: str, data: dict) -> T:
"""Update existing document."""
doc = self.get_or_throw(name, for_update=True)
doc.update(data)
doc.save()
return doc
def delete(self, name: str) -> None:
"""Delete document."""
frappe.delete_doc(self.doctype, name)
def get_value(
self,
name: str,
fieldname: str | list[str]
) -> Any:
"""Get specific field value(s) from document."""
return frappe.db.get_value(self.doctype, name, fieldname)
def set_value(self, name: str, fieldname: str, value: Any) -> None:
"""
Set specific field value directly in database.
WARNING: This bypasses controller validations and hooks.
Use doc.save() if you need validations to run.
"""
frappe.db.set_value(self.doctype, name, fieldname, value)<app_name>/<module>/repositories/base.py"""
Base repository class for data access operations.
Repositories handle all database interactions.
"""
import frappe
from frappe.query_builder import DocType
from typing import TYPE_CHECKING, Optional, Any, TypeVar, Generic
if TYPE_CHECKING:
from frappe.model.document import Document
T = TypeVar("T", bound="Document")
class BaseRepository(Generic[T]):
"""
Base class for all repository layer classes.
Repositories should:
- Handle all database operations
- Provide clean data access interface
- Abstract SQL/ORM details
- Never contain business logic
Performance Notes:
- Use get_cached() for repeated reads of same document
- Use get_value() when you only need 1-2 fields (faster than get_doc)
- get_list() applies user permissions; use get_all() to bypass (internal use only)
"""
doctype: str = ""
def __init__(self):
if not self.doctype:
raise ValueError("Repository must define doctype attribute")
def get(self, name: str, for_update: bool = False) -> Optional[T]:
"""
Get document by name. Fetches ALL fields and child tables.
For better performance when reading 1-2 fields, use get_value() instead.
For repeated reads of same document, use get_cached() instead.
"""
if not frappe.db.exists(self.doctype, name):
return None
return frappe.get_doc(self.doctype, name, for_update=for_update)
def get_cached(self, name: str) -> Optional[T]:
"""
Get document with caching. Use for repeated reads within same request.
Returns cached version if available, otherwise fetches and caches.
Cache is automatically invalidated when document is saved.
Can provide 10000x+ performance improvement for repeated reads.
"""
if not frappe.db.exists(self.doctype, name):
return None
return frappe.get_cached_doc(self.doctype, name)
def get_or_throw(self, name: str, for_update: bool = False) -> T:
"""Get document by name or throw if not found."""
doc = self.get(name, for_update=for_update)
if not doc:
frappe.throw(f"{self.doctype} {name} not found")
return doc
def exists(self, name: str) -> bool:
"""Check if document exists."""
return frappe.db.exists(self.doctype, name)
def get_list(
self,
filters: Optional[dict] = None,
fields: Optional[list[str]] = None,
order_by: str = "modified desc",
limit: int = 20,
offset: int = 0
) -> list[dict]:
"""
Get list of documents with user permission filtering.
Note: This applies user permissions automatically.
For internal/admin queries without permission checks, use get_all().
"""
return frappe.get_list(
self.doctype,
filters=filters,
fields=fields or ["name"],
order_by=order_by,
limit_page_length=limit,
limit_start=offset
)
def get_all(
self,
filters: Optional[dict] = None,
fields: Optional[list[str]] = None,
order_by: str = "modified desc",
limit: int = 20,
offset: int = 0
) -> list[dict]:
"""
Get list of documents WITHOUT permission filtering.
WARNING: Use only for internal/system operations.
For user-facing queries, use get_list() instead.
"""
return frappe.get_all(
self.doctype,
filters=filters,
fields=fields or ["name"],
order_by=order_by,
limit_page_length=limit,
limit_start=offset
)
def get_count(self, filters: Optional[dict] = None) -> int:
"""Get count of documents matching filters."""
return frappe.db.count(self.doctype, filters=filters)
def create(self, data: dict) -> T:
"""Create new document."""
doc = frappe.get_doc({"doctype": self.doctype, **data})
doc.insert()
return doc
def update(self, name: str, data: dict) -> T:
"""Update existing document."""
doc = self.get_or_throw(name, for_update=True)
doc.update(data)
doc.save()
return doc
def delete(self, name: str) -> None:
"""Delete document."""
frappe.delete_doc(self.doctype, name)
def get_value(
self,
name: str,
fieldname: str | list[str]
) -> Any:
"""Get specific field value(s) from document."""
return frappe.db.get_value(self.doctype, name, fieldname)
def set_value(self, name: str, fieldname: str, value: Any) -> None:
"""
Set specific field value directly in database.
WARNING: This bypasses controller validations and hooks.
Use doc.save() if you need validations to run.
"""
frappe.db.set_value(self.doctype, name, fieldname, value)<app_name>/tests/test_utils.py"""
Test utilities and fixtures for <app_name>.
"""
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
class <AppName>TestCase(IntegrationTestCase):
"""
Base test case for <app_name> integration tests.
Usage:
class TestMyFeature(<AppName>TestCase):
def test_something(self):
# Test with full database access
pass
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Setup test data
@classmethod
def tearDownClass(cls):
# Cleanup test data
super().tearDownClass()
def create_test_user(self, email: str, roles: list[str] = None) -> str:
"""Create a test user with specified roles."""
if frappe.db.exists("User", email):
return email
user = frappe.get_doc({
"doctype": "User",
"email": email,
"first_name": "Test",
"last_name": "User",
"send_welcome_email": 0
})
user.insert(ignore_permissions=True)
for role in (roles or []):
user.add_roles(role)
return email
class <AppName>UnitTestCase(UnitTestCase):
"""
Base test case for <app_name> unit tests (no database).
"""
pass<app_name>/tests/test_utils.py"""
Test utilities and fixtures for <app_name>.
"""
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
class <AppName>TestCase(IntegrationTestCase):
"""
Base test case for <app_name> integration tests.
Usage:
class TestMyFeature(<AppName>TestCase):
def test_something(self):
# Test with full database access
pass
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Setup test data
@classmethod
def tearDownClass(cls):
# Cleanup test data
super().tearDownClass()
def create_test_user(self, email: str, roles: list[str] = None) -> str:
"""Create a test user with specified roles."""
if frappe.db.exists("User", email):
return email
user = frappe.get_doc({
"doctype": "User",
"email": email,
"first_name": "Test",
"last_name": "User",
"send_welcome_email": 0
})
user.insert(ignore_permissions=True)
for role in (roles or []):
user.add_roles(role)
return email
class <AppName>UnitTestCase(UnitTestCase):
"""
Base test case for <app_name> unit tests (no database).
"""
passundefinedundefined
Wait for user confirmation.
等待用户确认。ls -la <app_name>/undefinedls -la <app_name>/undefinedbench get-app /path/to/<app_name>
bench --site <site> install-app <app_name>/frappe-doctype <doctype_name>/frappe-api <endpoint_name>bench --site <site> run-tests --app <app_name>undefinedbench get-app /path/to/<app_name>
bench --site <site> install-app <app_name>/frappe-doctype <doctype_name>/frappe-api <endpoint_name>bench --site <site> run-tests --app <app_name>undefined