selections/CLAUDE.md

132 lines
4.9 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
A Frappe/ERPNext app that replaces the standard "Add Multiple" button in sales documents (currently Sales Invoice) with a custom item selection dialog inspired by 1C-style item selection. The dialog provides enhanced filtering, real-time stock visibility, pricing, and a shopping cart interface.
## Development Commands
All `bench` commands run from `/home/frappe/frappe-bench`:
```bash
bench install-app selections # Install/reinstall
bench start # Development server
bench console # Python REPL with Frappe context
bench clear-cache # Required after JS/CSS/hook changes
bench migrate # Apply DB schema changes
bench --site [sitename] tail-logs # View error logs
```
### Code Quality
Pre-commit hooks (ruff, eslint, prettier, pyupgrade) — install once:
```bash
cd apps/selections && pre-commit install
```
Manual linting (from app directory):
```bash
ruff check . && ruff format .
npx eslint selections/public/js/**/*.js
npx prettier --write selections/public/js/**/*.js
```
Ruff: line length 110, tab indentation, import sorting enabled. See `pyproject.toml` for ignored rules.
## Architecture
### Key Components
**Backend API (`selections/api.py`)**:
- `get_items_for_selection(filters, start, page_length)`: Main endpoint. Fetches items with stock via LEFT JOIN on `Bin`, then enriches with prices per-item in Python.
- `get_item_groups_tree()`: Returns hierarchical item group tree for filtering. Cached in `frappe.cache()` for 5 minutes.
- `get_stock_across_warehouses(item_code)`: Returns per-warehouse stock for a single item (used for stock tooltip in dialog).
- `add_items_to_document(doctype, docname, items)`: Saves items server-side. Currently unused — the preferred flow adds items directly via JS.
**Frontend Dialog (`selections/public/js/selection_dialog.js`)**:
- `SelectionDialog` class: three-column layout (filters, items grid, cart). Exposed as `window.SelectionDialog`.
- Loaded globally via `app_include_js` in `hooks.py` — available on all desk pages.
**DocType Integration (`selections/public/js/sales_invoice.js`)**:
- Loaded per-doctype via `doctype_js` in `hooks.py`.
- On `refresh`, removes `.grid-add-multiple-rows` (Frappe's standard button) and injects a replacement button using `insertAfter` + `e.stopImmediatePropagation()` to prevent Frappe's own click handler from firing on the shared CSS class.
### Data Flow
1. User clicks "Add Multiple" in Sales Invoice → `SelectionDialog` instantiated
2. Dialog fetches warehouses, price lists, item groups via API
3. User filters → `get_items_for_selection()` queries DB and returns paginated results
4. Cart state managed client-side
5. Finalize → items added to form via `frappe.model.add_child()`, then `frm.doc.set_missing_values()` fills taxes/prices
### Price Lookup Chain
`get_price_data()` in `api.py` attempts pricing in this order:
1. Customer-specific `Item Price` record
2. General `Item Price` record (no customer)
3. ERPNext's `get_item_details()` for pricing rules/discounts (falls back gracefully on exception)
### Sorting Behavior
- Sort by `item_name` or `available_qty`: done at SQL level (works correctly across pages)
- Sort by `rate`: done in Python on the current page only — price data is fetched per-item after the SQL query
### Frappe Framework Patterns
**Whitelisted Methods**: `@frappe.whitelist()` required for any method callable from the client.
**Query Builder** (not raw SQL):
```python
Item = frappe.qb.DocType('Item')
query = frappe.qb.from_(Item).select(Item.name).where(Item.disabled == 0)
```
**Child Table Operations**:
```javascript
const row = frappe.model.add_child(frm.doc, 'Sales Invoice Item', 'items');
row.item_code = 'ITEM-001';
frm.refresh_field('items');
```
**Form Scripting**:
```javascript
frappe.ui.form.on('DocType Name', {
refresh: function(frm) { /* ... */ }
});
```
## Important Conventions
### Python
- Tabs for indentation (ruff-enforced)
- `from frappe.utils import flt, cstr` for numeric/string helpers
- `frappe.throw(_('Message'))` for user-facing errors; wrap all user strings with `_()`
- API docstrings are in Russian (existing codebase convention)
### JavaScript
- Frappe globals available: `frappe`, `__()`, `flt()`, `format_currency()`, `$` (jQuery)
- `frappe.show_alert()` for transient notifications; `frappe.msgprint()` for blocking messages
## Extending to Other DocTypes
1. Add to `doctype_js` in `hooks.py`:
```python
doctype_js = {
"Sales Invoice": "public/js/sales_invoice.js",
"Sales Order": "public/js/sales_order.js"
}
```
2. Create the JS file following the pattern in `sales_invoice.js`
3. Pass the correct child table name to `SelectionDialog` (e.g., `'Sales Order Item'`)
## After Making Changes
```bash
bench clear-cache # Always required for JS/CSS/hook changes
# Then hard-refresh browser (Ctrl+Shift+R)
```