Business-M Example Package
This is a complete example of a Framework M package with frontend components.
Package Structure (Monorepo)
business-m/
├── pyproject.toml # Workspace configuration
├── LICENSE
├── README.md
├── apps/
│ ├── business-m/ # Main application
│ │ ├── pyproject.toml
│ │ ├── src/business_m/
│ │ │ ├── doctypes/
│ │ │ └── seed.py
│ │ ├── tests/
│ │ └── frontend/ # Frontend shell
│ ├── book-keeper/ # Double-entry ledger service
│ └── stock-keeper/ # Inventory management service
└── libs/
├── finance/ # Shared finance logic
├── wms/ # Warehouse management logic
└── people/ # HR and payroll logic
Workspace Configuration
pyproject.toml (Root)
[project]
name = "business-m-root"
version = "0.0.0"
requires-python = ">=3.12"
dependencies = [
"framework-m>=0.2.5",
]
[tool.uv.workspace]
members = ["apps/*", "libs/*"]
apps/business-m/pyproject.toml
[project]
name = "business_m"
version = "0.1.0"
description = "Core business entities for Framework M"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"framework-m>=0.4.15",
]
[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"]
Backend Code
apps/business-m/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__"]
apps/business-m/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"],
}
apps/business-m/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
apps/business-m/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"
}
}
apps/business-m/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";
apps/business-m/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>
);
}
apps/business-m/frontend/pages/CustomerList.tsx
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>
);
}
apps/business-m/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"] });
},
});
}
apps/business-m/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://gitlab.com/castlecraft/framework-m/business-m.git
cd business-m
# Sync workspace using uv
uv sync
# Run tests
uv run pytest
# Run dev server
pnpm -F business-m dev
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:
// In crm-app package
import { CustomerCard } from "@business-m/frontend";
import { LeadInfo } from "./components/LeadInfo";
function CRMCustomerCard(props) {
return (
<div>
<CustomerCard {...props} />
<LeadInfo customerId={props.customer.id} />
</div>
);
}