4.9 KiB
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:
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:
cd apps/selections && pre-commit install
Manual linting (from app directory):
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 onBin, then enriches with prices per-item in Python.get_item_groups_tree(): Returns hierarchical item group tree for filtering. Cached infrappe.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):
SelectionDialogclass: three-column layout (filters, items grid, cart). Exposed aswindow.SelectionDialog.- Loaded globally via
app_include_jsinhooks.py— available on all desk pages.
DocType Integration (selections/public/js/sales_invoice.js):
- Loaded per-doctype via
doctype_jsinhooks.py. - On
refresh, removes.grid-add-multiple-rows(Frappe's standard button) and injects a replacement button usinginsertAfter+e.stopImmediatePropagation()to prevent Frappe's own click handler from firing on the shared CSS class.
Data Flow
- User clicks "Add Multiple" in Sales Invoice →
SelectionDialoginstantiated - Dialog fetches warehouses, price lists, item groups via API
- User filters →
get_items_for_selection()queries DB and returns paginated results - Cart state managed client-side
- Finalize → items added to form via
frappe.model.add_child(), thenfrm.doc.set_missing_values()fills taxes/prices
Price Lookup Chain
get_price_data() in api.py attempts pricing in this order:
- Customer-specific
Item Pricerecord - General
Item Pricerecord (no customer) - ERPNext's
get_item_details()for pricing rules/discounts (falls back gracefully on exception)
Sorting Behavior
- Sort by
item_nameoravailable_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):
Item = frappe.qb.DocType('Item')
query = frappe.qb.from_(Item).select(Item.name).where(Item.disabled == 0)
Child Table Operations:
const row = frappe.model.add_child(frm.doc, 'Sales Invoice Item', 'items');
row.item_code = 'ITEM-001';
frm.refresh_field('items');
Form Scripting:
frappe.ui.form.on('DocType Name', {
refresh: function(frm) { /* ... */ }
});
Important Conventions
Python
- Tabs for indentation (ruff-enforced)
from frappe.utils import flt, cstrfor numeric/string helpersfrappe.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
- Add to
doctype_jsinhooks.py:
doctype_js = {
"Sales Invoice": "public/js/sales_invoice.js",
"Sales Order": "public/js/sales_order.js"
}
- Create the JS file following the pattern in
sales_invoice.js - Pass the correct child table name to
SelectionDialog(e.g.,'Sales Order Item')
After Making Changes
bench clear-cache # Always required for JS/CSS/hook changes
# Then hard-refresh browser (Ctrl+Shift+R)