Skip to main content

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>
);
}