Skip to main content

Business-M Example Package

This is a complete example of a Framework M package with frontend components.

Package Structure

business-m/
├── LICENSE
├── README.md
├── pyproject.toml
├── tests/
│ ├── __init__.py
│ ├── test_customer.py
│ └── test_invoice.py
└── src/
└── business_m/
├── __init__.py
├── doctypes/
│ ├── __init__.py
│ ├── customer.py
│ └── invoice.py
└── frontend/
├── index.ts
├── package.json
├── tsconfig.json
├── components/
│ ├── CustomerCard.tsx
│ ├── CustomerCard.module.css
│ ├── InvoiceForm.tsx
│ └── InvoiceForm.module.css
├── pages/
│ ├── CustomerList.tsx
│ ├── CustomerDetail.tsx
│ ├── InvoiceList.tsx
│ └── InvoiceDetail.tsx
├── hooks/
│ ├── useCustomer.ts
│ └── useInvoice.ts
└── types/
└── index.ts

pyproject.toml

[build-system]
requires = [\"hatchling\"]
build-backend = \"hatchling.build\"

[project]
name = \"business-m\"
version = \"1.0.0\"
description = \"Core business entities for Framework M\"
readme = \"README.md\"
requires-python = \">=3.12\"
license = \"MIT\"
authors = [
{ name = \"Framework M Team\", email = \"team@framework-m.dev\" }
]
classifiers = [
\"Development Status :: 4 - Beta\",
\"Framework :: AsyncIO\",
\"Intended Audience :: Developers\",
\"License :: OSI Approved :: Apache 2.0 License\",
\"Programming Language :: Python :: 3.12\",
]
dependencies = [
\"framework-m>=0.1.0\",
]

[project.optional-dependencies]
dev = [
\"pytest>=7.0.0\",
\"pytest-asyncio>=0.21.0\",
\"mypy>=1.0.0\",
\"ruff>=0.1.0\",
]

[project.entry-points.\"framework_m.apps\"]
business_m = \"business_m:app\"

[project.entry-points.\"framework_m.frontend\"]
business_m = \"business_m.frontend:plugin\"

[tool.hatch.build.targets.wheel]
packages = [\"src/business_m\"]

[tool.pytest.ini_options]
testpaths = [\"tests\"]
asyncio_mode = \"auto\"

[tool.mypy]
strict = true
warn_return_any = true
warn_unused_configs = true

[tool.ruff]
line-length = 88
target-version = \"py312\"

Backend Code

src/business_m/init.py

\"\"\"Business-M: Core business entities for Framework M.\"\"\"

from framework_m import App

app = App(
name=\"business_m\",
doctypes_package=\"business_m.doctypes\",
)

__version__ = \"1.0.0\"
__all__ = [\"app\", \"__version__\"]

src/business_m/doctypes/customer.py

\"\"\"Customer DocType.\"\"\"

from typing import Optional
from framework_m import DocType, Field


class Customer(DocType):
\"\"\"Represents a business customer.\"\"\"

customer_name: str = Field(title=\"Customer Name\", req=True)
email: Optional[str] = Field(title=\"Email\", unique=True)
phone: Optional[str] = Field(title=\"Phone\")
address: Optional[str] = Field(title=\"Address\")
credit_limit: float = Field(default=0.0, title=\"Credit Limit\")
outstanding_amount: float = Field(default=0.0, title=\"Outstanding Amount\")

class Meta:
doctype = \"Customer\"
title_field = \"customer_name\"
search_fields = [\"customer_name\", \"email\", \"phone\"]
roles = {
\"read\": [\"Sales User\", \"Sales Manager\"],
\"write\": [\"Sales Manager\"],
\"create\": [\"Sales Manager\"],
\"delete\": [\"Sales Manager\"],
}

src/business_m/doctypes/invoice.py

\"\"\"Invoice DocType.\"\"\"

from typing import Optional
from datetime import date
from framework_m import DocType, Field


class Invoice(DocType):
\"\"\"Represents a sales invoice.\"\"\"

invoice_number: str = Field(title=\"Invoice Number\", req=True, unique=True)
customer_id: str = Field(title=\"Customer\", req=True, link=\"Customer\")
invoice_date: date = Field(title=\"Invoice Date\", req=True)
due_date: date = Field(title=\"Due Date\", req=True)
total_amount: float = Field(default=0.0, title=\"Total Amount\")
paid_amount: float = Field(default=0.0, title=\"Paid Amount\")
status: str = Field(default=\"Draft\", title=\"Status\")

class Meta:
doctype = \"Invoice\"
title_field = \"invoice_number\"
search_fields = [\"invoice_number\", \"customer_id\"]
submittable = True
roles = {
\"read\": [\"Sales User\", \"Sales Manager\"],
\"write\": [\"Sales User\", \"Sales Manager\"],
\"create\": [\"Sales User\", \"Sales Manager\"],
\"submit\": [\"Sales Manager\"],
}

Frontend Code

frontend/package.json

{
\"name\": \"@business-m/frontend\",
\"version\": \"1.0.0\",
\"private\": true,
\"peerDependencies\": {
\"react\": \"^18.0.0\",
\"react-dom\": \"^18.0.0\",
\"@framework-m/core\": \"^0.1.0\"
},
\"dependencies\": {
\"date-fns\": \"^2.30.0\",
\"react-hook-form\": \"^7.49.0\",
\"zod\": \"^3.22.0\"
},
\"devDependencies\": {
\"@types/react\": \"^18.2.0\",
\"@types/react-dom\": \"^18.2.0\",
\"typescript\": \"^5.3.0\"
}
}

frontend/index.ts

import { registerPlugin } from \"@framework-m/core\";
import { CustomerCard } from \"./components/CustomerCard\";
import { InvoiceForm } from \"./components/InvoiceForm\";
import { CustomerListPage } from \"./pages/CustomerList\";
import { CustomerDetailPage } from \"./pages/CustomerDetail\";
import { InvoiceListPage } from \"./pages/InvoiceList\";
import { InvoiceDetailPage } from \"./pages/InvoiceDetail\";

export const plugin = {
name: \"business_m\",
version: \"1.0.0\",
};

registerPlugin({
name: \"business_m\",
version: \"1.0.0\",

pages: [
{
path: \"/app/customer/list\",
component: CustomerListPage,
},
{
path: \"/app/customer/:id\",
component: CustomerDetailPage,
},
{
path: \"/app/invoice/list\",
component: InvoiceListPage,
},
{
path: \"/app/invoice/:id\",
component: InvoiceDetailPage,
},
],

components: {
CustomerCard,
InvoiceForm,
},
});

// Export public API for other packages
export { CustomerCard } from \"./components/CustomerCard\";
export { InvoiceForm } from \"./components/InvoiceForm\";
export { useCustomer } from \"./hooks/useCustomer\";
export { useInvoice } from \"./hooks/useInvoice\";
export type { CustomerCardProps, InvoiceFormProps } from \"./types\";

frontend/components/CustomerCard.tsx

import { Card, Badge } from \"@framework-m/core\";
import type { CustomerCardProps } from \"../types\";
import styles from \"./CustomerCard.module.css\";

export function CustomerCard({ customer, variant = \"detailed\" }: CustomerCardProps) {
const isOverLimit = customer.outstanding_amount > customer.credit_limit;

return (
<Card className={styles.card}>
<div className={styles.header}>
<h3>{customer.customer_name}</h3>
{isOverLimit && <Badge variant=\"danger\">Over Limit</Badge>}
</div>

{variant === \"detailed\" && (
<div className={styles.details}>
<div className={styles.row}>
<span>Email:</span>
<span>{customer.email || \"—\"}</span>
</div>
<div className={styles.row}>
<span>Phone:</span>
<span>{customer.phone || \"—\"}</span>
</div>
<div className={styles.row}>
<span>Credit Limit:</span>
<span>${customer.credit_limit.toFixed(2)}</span>
</div>
<div className={styles.row}>
<span>Outstanding:</span>
<span className={isOverLimit ? styles.danger : \"\"}>
${customer.outstanding_amount.toFixed(2)}
</span>
</div>
</div>
)}
</Card>
);
}\n```

### frontend/pages/CustomerList.tsx

```typescript
import { Page, DataTable, Button } from \"@framework-m/core\";
import { useNavigate } from \"react-router-dom\";
import { useCustomers } from \"../hooks/useCustomer\";

export function CustomerListPage() {
const navigate = useNavigate();
const { data: customers, isLoading } = useCustomers();

return (
<Page
title=\"Customers\"
actions={
<Button onClick={() => navigate(\"/app/customer/new\")}>
New Customer
</Button>
}
>
<DataTable
data={customers}
loading={isLoading}
columns={[
{ key: \"customer_name\", label: \"Name\" },
{ key: \"email\", label: \"Email\" },
{ key: \"phone\", label: \"Phone\" },
{
key: \"outstanding_amount\",
label: \"Outstanding\",
render: (value) => `$${value.toFixed(2)}`,
},
]}
onRowClick={(customer) => navigate(`/app/customer/${customer.id}`)}
/>
</Page>
);
}

frontend/hooks/useCustomer.ts

import { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";
import { api } from \"@framework-m/core\";
import type { Customer } from \"../types\";

export function useCustomer(id: string) {
return useQuery<Customer>({
queryKey: [\"customer\", id],
queryFn: () => api.get(`/api/customer/${id}`),
});
}

export function useCustomers() {
return useQuery<Customer[]>({
queryKey: [\"customers\"],
queryFn: () => api.get(\"/api/customer\"),
});
}

export function useUpdateCustomer() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (customer: Customer) =>
api.put(`/api/customer/${customer.id}`, customer),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [\"customers\"] });
},
});
}

frontend/types/index.ts

export interface Customer {
id: string;
customer_name: string;
email?: string;
phone?: string;
address?: string;
credit_limit: number;
outstanding_amount: number;
}

export interface Invoice {
id: string;
invoice_number: string;
customer_id: string;
invoice_date: string;
due_date: string;
total_amount: number;
paid_amount: number;
status: \"Draft\" | \"Submitted\" | \"Paid\" | \"Cancelled\";
}

export interface CustomerCardProps {
customer: Customer;
variant?: \"compact\" | \"detailed\";
onUpdate?: (customer: Customer) => void;
}

export interface InvoiceFormProps {
invoice?: Invoice;
onSave: (invoice: Invoice) => void;
onCancel: () => void;
}

Installation

Development

# Clone repository
git clone https://github.com/framework-m/business-m.git
cd business-m

# Install in editable mode
pip install -e \".[dev]\"

# Run tests
pytest

# Type check
mypy src/business_m

Production

pip install business-m

Usage

In Python

from business_m.doctypes.customer import Customer
from business_m.doctypes.invoice import Invoice

# DocTypes are auto-discovered via entry point

In Frontend

import { CustomerCard, useCustomer } from \"@business-m/frontend\";

function MyComponent() {
const { data: customer } = useCustomer(\"CUST-001\");

return <CustomerCard customer={customer} variant=\"detailed\" />;
}

Extending

Other packages can extend business-m:\n\ntypescript\n// In crm-app package\nimport { CustomerCard } from \"@business-m/frontend\";\nimport { LeadInfo } from \"./components/LeadInfo\";\n\nfunction CRMCustomerCard(props) {\n return (\n <div>\n <CustomerCard {...props} />\n <LeadInfo customerId={props.customer.id} />\n </div>\n );\n}\n\n\n## License\n\nMIT\n