diff --git a/.agents/skills/ai-vibe-slides/SKILL.md b/.agents/skills/ai-vibe-slides/SKILL.md new file mode 100644 index 00000000..160e1f9a --- /dev/null +++ b/.agents/skills/ai-vibe-slides/SKILL.md @@ -0,0 +1,453 @@ +--- +name: ai-vibe-slides +description: "Create beautiful, professional HTML or React slide decks ready for fullscreen presentation. Use this skill when the user wants to: create a PPT/slide/presentation from an idea or outline; build a visually stunning slide deck from existing content; generate an HTML presentation that can be projected fullscreen; convert a document into a presentation. Trigger when you hear: 'create slides', 'make a PPT', 'presentation', 'slide deck', 'pitch deck', 'vibe ppt', 'make a talk', or any request to create a presentation. This skill produces a single self-contained HTML/React artifact — no backend, no installation, no dependencies." +--- + +# AI Vibe Slides — Beautiful HTML Slide Decks, Ready to Present + +## Goal + +Produce **a single HTML or React artifact file** containing a complete slide deck that can: + +- Present fullscreen directly in the browser +- Navigate via arrow keys, spacebar, or click +- Look professional, polished, and stylistically consistent +- Print or export to PDF when needed + +Inspired by [banana-slides](https://github.com/Anionex/banana-slides): a 3-step pipeline of **Idea → Outline → Finished Slides**, but the output is a self-contained HTML file instead of a fullstack application. + +--- + +## Slide Creation Pipeline + +### Step 1: Understand the Request → Build an Outline + +When the user provides a request, first **build an outline mentally** (no need to display it unless the user asks): + +1. Identify the **main topic** and **target audience** (students, business, tech talk...) +2. Break it into **logical sections**: + - Slide 1: Cover (title + subtitle + author) + - Slides 2-3: Introduction / context + - Middle slides: Core content (one key idea per slide) + - Final slide: Conclusion / Call to action / Thank you +3. Each slide should have: a title, 2-5 bullet points or visual content, and a layout type + +If the user only gives a short sentence (e.g., "create slides about AI in healthcare"), automatically expand it into 8-12 slides with a logical structure. + +### Step 2: Choose a Design Direction + +Based on context, commit to **one clear design direction**: + +| Context | Suggested Style | +| ---------------------- | ----------------------------------------------------- | +| Startup pitch deck | Bold, dark theme, gradient accents, strong sans-serif | +| Academic / education | Clean, light, diagram-heavy, readable fonts | +| Tech talk / conference | Modern dark, code-style typography, neon accents | +| Corporate / report | Minimal, professional, navy/white, serif headings | +| Creative / marketing | Colorful, asymmetric layout, bold typography | +| Kids / early education | Pastel, rounded corners, playful icons, large text | + +General rules: + +- **Pick 2-3 primary colors** and use them consistently across the entire deck +- **1 heading font + 1 body font** (use Google Fonts) +- **Minimal text, generous whitespace** — max 5-6 lines per slide +- **Clear visual hierarchy**: large title → medium content → small notes + +### Step 3: Generate the HTML/React Slide Deck + +Create **a single file** (`.html` or `.jsx`) containing everything: + +--- + +## HTML Slide Deck Structure + +```html + + + + + + {Presentation Title} + + + + +
+ +
+
Topic
+

Main Title

+

+ Short subtitle description +

+
+

+ Author — Date +

+
+ + +
+

Slide Title

+
+ +
+ + +
+ +
+ 1 / +
+ + + + + +``` + +--- + +## Slide Layout Types + +Each slide should use the layout that best fits its content: + +### 1. Cover Slide + +- Centered, extra-large font, gradient background +- Topic badge + main title + subtitle + author + +### 2. Section Divider + +- Only section title + number, accent color background +- Used to separate major sections of the presentation + +### 3. Content + Bullets + +- Left-aligned title + list of key points +- Use icons/emoji at the start of each bullet instead of plain dots + +### 4. Two-Column + +- `.two-column` layout: text on left, visuals/list/cards on right +- Great for comparisons, before-after, text+illustration + +### 5. Cards Grid + +- 2-3 column grid, each card containing icon + title + short description +- Great for features, benefits, team members + +### 6. Big Number / Statistic + +- Huge number in the center (font-size: 5rem+) + small label below +- Great for data points, KPIs, impact numbers + +### 7. Quote / Highlight + +- Large text, centered, with decorative quotation marks +- Different background (light accent color) + +### 8. Timeline / Steps + +- Horizontal or vertical flexbox, dots connecting each step +- Great for processes, roadmaps, history + +### 9. Thank You / CTA + +- Centered, simple, contact info or call to action + +--- + +## MANDATORY Design Rules + +1. **16:9 aspect ratio**: Always use `width: 100vw; height: 100vh` — each slide fills the entire screen +2. **Minimal text**: Maximum 6 lines per slide. If content is long → split into multiple slides +3. **Large font sizes**: Heading ≥ 2.4rem, body ≥ 1.3rem — must be readable on a projector +4. **High contrast**: Text must be clearly legible against its background. Verify visually +5. **Consistency**: Same fonts, same color palette, same spacing throughout the entire deck +6. **No placeholder images**: Never use ``. Replace with CSS shapes, gradients, icons (emoji or Lucide for React), or inline SVGs +7. **Subtle animation**: Only fadeIn on slide transition. No complex animations that distract +8. **Responsive fullscreen**: Must work well at all screen sizes +9. **Print-ready**: Include `@media print` rules so each slide becomes one printed page + +--- + +## When Using React (.jsx) Instead of HTML + +If creating a React artifact, use this pattern: + +```jsx +import { useState, useEffect, useCallback } from "react"; + +const slides = [ + { type: "cover", title: "...", subtitle: "..." }, + { type: "content", title: "...", points: ["...", "..."] }, + { type: "twoColumn", title: "...", left: "...", right: "..." }, + // ... +]; + +export default function SlideDeck() { + const [current, setCurrent] = useState(0); + + const next = useCallback( + () => setCurrent((c) => (c + 1) % slides.length), + [], + ); + const prev = useCallback( + () => setCurrent((c) => (c - 1 + slides.length) % slides.length), + [], + ); + + useEffect(() => { + const handleKey = (e) => { + if (e.key === "ArrowRight" || e.key === " ") next(); + if (e.key === "ArrowLeft") prev(); + if (e.key === "f") document.documentElement.requestFullscreen?.(); + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [next, prev]); + + const renderSlide = (slide) => { + switch (slide.type) { + case "cover": + return /* cover layout */; + case "content": + return /* content layout */; + case "twoColumn": + return /* two-column layout */; + // ... + } + }; + + return ( +
(e.clientX > window.innerWidth / 2 ? next() : prev())} + style={{ fontFamily: "'Outfit', sans-serif" }} + > + {renderSlide(slides[current])} +
+ {current + 1} / {slides.length} +
+
+ ); +} +``` + +Advantages of React: Tailwind CSS utilities, Lucide icons, more complex logic, recharts for charts. + +--- + +## Pre-Delivery Checklist + +- [ ] Correct number of slides (cover + content + closing) +- [ ] Arrow keys ← → work, click navigates +- [ ] Counter displays correct "X / N" +- [ ] Press F for fullscreen +- [ ] All text is readable with sufficient contrast +- [ ] No broken images or placeholders +- [ ] Google Fonts load correctly (or good fallback) +- [ ] Print/PDF exports properly (`@media print`) +- [ ] Consistent style throughout the entire deck +- [ ] Each slide has exactly one key idea, not overloaded with text + +--- + +## Natural Language Editing + +After initial creation, the user can request modifications: + +- "Switch to a light theme" → update CSS variables +- "Add 2 slides about case studies" → insert slides into the array +- "Slide 3 has too much text, split it" → refactor content across slides +- "Change the font to Playfair Display" → update Google Fonts link + CSS +- "Add a chart to slide 5" → use CSS chart or recharts (React) + +On each edit, keep unchanged slides intact and only update what the user requested. diff --git a/.agents/skills/brainstorming/SKILL.md b/.agents/skills/brainstorming/SKILL.md new file mode 100644 index 00000000..1dd476ef --- /dev/null +++ b/.agents/skills/brainstorming/SKILL.md @@ -0,0 +1,69 @@ +--- +name: brainstorming +description: "Use when defining or changing features, components, behavior, or architecture and requirements are still fuzzy. Helps clarify intent, constraints, trade-offs, and success criteria before implementation." +--- + +# Brainstorming Ideas Into Designs + +## Overview + +Help turn ideas into fully formed designs and specs through natural collaborative dialogue. + +Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far. + +For Codoo work, end with concrete outputs that can be executed deterministically: +- feature spec candidate (`docs/features/spec-FEAT-[ID].yaml`) +- impacted files map +- validation gate plan (install, API, UI, permissions, evidence) + +## The Process + +**Understanding the idea:** +- Check out the current project state first (files, docs, recent commits) +- Ask questions one at a time to refine the idea +- Prefer multiple choice questions when possible, but open-ended is fine too +- Only one question per message - if a topic needs more exploration, break it into multiple questions +- Focus on understanding: purpose, constraints, success criteria + +**Exploring approaches:** +- Propose 2-3 different approaches with trade-offs +- Present options conversationally with your recommendation and reasoning +- Lead with your recommended option and explain why +- Explicitly call out SaaS constraints when Odoo server-side customization may be limited + +**Presenting the design:** +- Once you believe you understand what you're building, present the design +- Break it into sections of 200-300 words +- Ask after each section whether it looks right so far +- Cover: architecture, components, data flow, error handling, testing +- Cover: evidence strategy and rollback/fallback strategy +- Be ready to go back and clarify if something doesn't make sense + +## After the Design + +**Documentation:** +- Write the validated design to `docs/plans/YYYY-MM-DD--design.md` +- Commit the design document to git + +**Implementation (if continuing):** +- Ask: "Ready to set up for implementation?" +- Use superpowers:using-git-worktrees to create isolated workspace +- Use superpowers:writing-plans to create detailed implementation plan + +## Key Principles + +- **One question at a time** - Don't overwhelm with multiple questions +- **Multiple choice preferred** - Easier to answer than open-ended when possible +- **YAGNI ruthlessly** - Remove unnecessary features from all designs +- **Explore alternatives** - Always propose 2-3 approaches before settling +- **Incremental validation** - Present design in sections, validate each +- **Be flexible** - Go back and clarify when something doesn't make sense +- **Execution-ready outputs** - End brainstorming with inputs ready for implementation gates + +## Exit Criteria + +Brainstorming is complete only when these are explicit: +- Problem statement and success criteria +- Preferred approach and rejected alternatives +- Constraints and assumptions +- Initial validation plan \ No newline at end of file diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md new file mode 100644 index 00000000..ba2127b1 --- /dev/null +++ b/.agents/skills/code-review/SKILL.md @@ -0,0 +1,140 @@ +--- +name: code-review +description: Use when receiving code review feedback (especially if unclear or technically questionable), when completing tasks or major features requiring review before proceeding, or before making any completion/success claims. Covers three practices - receiving feedback with technical rigor over performative agreement, requesting reviews via code-reviewer subagent, and verification gates requiring evidence before any status claims. Essential for subagent-driven development, pull requests, and preventing false completion claims. +--- + +# Code Review + +Guide proper code review practices emphasizing technical rigor, evidence-based claims, and verification over performative responses. + +## Overview + +Code review requires three distinct practices: + +1. **Receiving feedback** - Technical evaluation over performative agreement +2. **Requesting reviews** - Systematic review via code-reviewer subagent +3. **Verification gates** - Evidence before any completion claims + +Each practice has specific triggers and protocols detailed in reference files. + +## Core Principle + +**Technical correctness over social comfort.** Verify before implementing. Ask before assuming. Evidence before claims. + +## When to Use This Skill + +### Receiving Feedback +Trigger when: +- Receiving code review comments from any source +- Feedback seems unclear or technically questionable +- Multiple review items need prioritization +- External reviewer lacks full context +- Suggestion conflicts with existing decisions + +**Reference:** `references/code-review-reception.md` + +### Requesting Review +Trigger when: +- Completing tasks in subagent-driven development (after EACH task) +- Finishing major features or refactors +- Before merging to main branch +- Stuck and need fresh perspective +- After fixing complex bugs + +**Reference:** `references/requesting-code-review.md` + +### Verification Gates +Trigger when: +- About to claim tests pass, build succeeds, or work is complete +- Before committing, pushing, or creating PRs +- Moving to next task +- Any statement suggesting success/completion +- Expressing satisfaction with work + +**Reference:** `references/verification-before-completion.md` + +## Quick Decision Tree + +``` +SITUATION? +│ +├─ Received feedback +│ ├─ Unclear items? → STOP, ask for clarification first +│ ├─ From human partner? → Understand, then implement +│ └─ From external reviewer? → Verify technically before implementing +│ +├─ Completed work +│ ├─ Major feature/task? → Request code-reviewer subagent review +│ └─ Before merge? → Request code-reviewer subagent review +│ +└─ About to claim status + ├─ Have fresh verification? → State claim WITH evidence + └─ No fresh verification? → RUN verification command first +``` + +## Receiving Feedback Protocol + +### Response Pattern +READ → UNDERSTAND → VERIFY → EVALUATE → RESPOND → IMPLEMENT + +### Key Rules +- ❌ No performative agreement: "You're absolutely right!", "Great point!", "Thanks for [anything]" +- ❌ No implementation before verification +- ✅ Restate requirement, ask questions, push back with technical reasoning, or just start working +- ✅ If unclear: STOP and ask for clarification on ALL unclear items first +- ✅ YAGNI check: grep for usage before implementing suggested "proper" features + +### Source Handling +- **Human partner:** Trusted - implement after understanding, no performative agreement +- **External reviewers:** Verify technically correct, check for breakage, push back if wrong + +**Full protocol:** `references/code-review-reception.md` + +## Requesting Review Protocol + +### When to Request +- After each task in subagent-driven development +- After major feature completion +- Before merge to main + +### Process +1. Get git SHAs: `BASE_SHA=$(git rev-parse HEAD~1)` and `HEAD_SHA=$(git rev-parse HEAD)` +2. Dispatch code-reviewer subagent via Task tool with: WHAT_WAS_IMPLEMENTED, PLAN_OR_REQUIREMENTS, BASE_SHA, HEAD_SHA, DESCRIPTION +3. Act on feedback: Fix Critical immediately, Important before proceeding, note Minor for later + +**Full protocol:** `references/requesting-code-review.md` + +## Verification Gates Protocol + +### The Iron Law +**NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE** + +### Gate Function +IDENTIFY command → RUN full command → READ output → VERIFY confirms claim → THEN claim + +Skip any step = lying, not verifying + +### Requirements +- Tests pass: Test output shows 0 failures +- Build succeeds: Build command exit 0 +- Bug fixed: Test original symptom passes +- Requirements met: Line-by-line checklist verified + +### Red Flags - STOP +Using "should"/"probably"/"seems to", expressing satisfaction before verification, committing without verification, trusting agent reports, ANY wording implying success without running verification + +**Full protocol:** `references/verification-before-completion.md` + +## Integration with Workflows + +- **Subagent-Driven:** Review after EACH task, verify before moving to next +- **Pull Requests:** Verify tests pass, request code-reviewer review before merge +- **General:** Apply verification gates before any status claims, push back on invalid feedback + +## Bottom Line + +1. Technical rigor over social performance - No performative agreement +2. Systematic review processes - Use code-reviewer subagent +3. Evidence before claims - Verification gates always + +Verify. Question. Then implement. Evidence. Then claim. diff --git a/.agents/skills/code-review/references/code-review-reception.md b/.agents/skills/code-review/references/code-review-reception.md new file mode 100644 index 00000000..c52443d3 --- /dev/null +++ b/.agents/skills/code-review/references/code-review-reception.md @@ -0,0 +1,209 @@ +--- +name: receiving-code-review +description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation +--- + +# Code Review Reception + +## Overview + +Code review requires technical evaluation, not emotional performance. + +**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort. + +## The Response Pattern + +``` +WHEN receiving code review feedback: + +1. READ: Complete feedback without reacting +2. UNDERSTAND: Restate requirement in own words (or ask) +3. VERIFY: Check against codebase reality +4. EVALUATE: Technically sound for THIS codebase? +5. RESPOND: Technical acknowledgment or reasoned pushback +6. IMPLEMENT: One item at a time, test each +``` + +## Forbidden Responses + +**NEVER:** +- "You're absolutely right!" (explicit CLAUDE.md violation) +- "Great point!" / "Excellent feedback!" (performative) +- "Let me implement that now" (before verification) + +**INSTEAD:** +- Restate the technical requirement +- Ask clarifying questions +- Push back with technical reasoning if wrong +- Just start working (actions > words) + +## Handling Unclear Feedback + +``` +IF any item is unclear: + STOP - do not implement anything yet + ASK for clarification on unclear items + +WHY: Items may be related. Partial understanding = wrong implementation. +``` + +**Example:** +``` +your human partner: "Fix 1-6" +You understand 1,2,3,6. Unclear on 4,5. + +❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later +✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding." +``` + +## Source-Specific Handling + +### From your human partner +- **Trusted** - implement after understanding +- **Still ask** if scope unclear +- **No performative agreement** +- **Skip to action** or technical acknowledgment + +### From External Reviewers +``` +BEFORE implementing: + 1. Check: Technically correct for THIS codebase? + 2. Check: Breaks existing functionality? + 3. Check: Reason for current implementation? + 4. Check: Works on all platforms/versions? + 5. Check: Does reviewer understand full context? + +IF suggestion seems wrong: + Push back with technical reasoning + +IF can't easily verify: + Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?" + +IF conflicts with your human partner's prior decisions: + Stop and discuss with your human partner first +``` + +**your human partner's rule:** "External feedback - be skeptical, but check carefully" + +## YAGNI Check for "Professional" Features + +``` +IF reviewer suggests "implementing properly": + grep codebase for actual usage + + IF unused: "This endpoint isn't called. Remove it (YAGNI)?" + IF used: Then implement properly +``` + +**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it." + +## Implementation Order + +``` +FOR multi-item feedback: + 1. Clarify anything unclear FIRST + 2. Then implement in this order: + - Blocking issues (breaks, security) + - Simple fixes (typos, imports) + - Complex fixes (refactoring, logic) + 3. Test each fix individually + 4. Verify no regressions +``` + +## When To Push Back + +Push back when: +- Suggestion breaks existing functionality +- Reviewer lacks full context +- Violates YAGNI (unused feature) +- Technically incorrect for this stack +- Legacy/compatibility reasons exist +- Conflicts with your human partner's architectural decisions + +**How to push back:** +- Use technical reasoning, not defensiveness +- Ask specific questions +- Reference working tests/code +- Involve your human partner if architectural + +**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K" + +## Acknowledging Correct Feedback + +When feedback IS correct: +``` +✅ "Fixed. [Brief description of what changed]" +✅ "Good catch - [specific issue]. Fixed in [location]." +✅ [Just fix it and show in the code] + +❌ "You're absolutely right!" +❌ "Great point!" +❌ "Thanks for catching that!" +❌ "Thanks for [anything]" +❌ ANY gratitude expression +``` + +**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback. + +**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead. + +## Gracefully Correcting Your Pushback + +If you pushed back and were wrong: +``` +✅ "You were right - I checked [X] and it does [Y]. Implementing now." +✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing." + +❌ Long apology +❌ Defending why you pushed back +❌ Over-explaining +``` + +State the correction factually and move on. + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Performative agreement | State requirement or just act | +| Blind implementation | Verify against codebase first | +| Batch without testing | One at a time, test each | +| Assuming reviewer is right | Check if breaks things | +| Avoiding pushback | Technical correctness > comfort | +| Partial implementation | Clarify all items first | +| Can't verify, proceed anyway | State limitation, ask for direction | + +## Real Examples + +**Performative Agreement (Bad):** +``` +Reviewer: "Remove legacy code" +❌ "You're absolutely right! Let me remove that..." +``` + +**Technical Verification (Good):** +``` +Reviewer: "Remove legacy code" +✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?" +``` + +**YAGNI (Good):** +``` +Reviewer: "Implement proper metrics tracking with database, date filters, CSV export" +✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?" +``` + +**Unclear Item (Good):** +``` +your human partner: "Fix items 1-6" +You understand 1,2,3,6. Unclear on 4,5. +✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing." +``` + +## The Bottom Line + +**External feedback = suggestions to evaluate, not orders to follow.** + +Verify. Question. Then implement. + +No performative agreement. Technical rigor always. \ No newline at end of file diff --git a/.agents/skills/code-review/references/requesting-code-review.md b/.agents/skills/code-review/references/requesting-code-review.md new file mode 100644 index 00000000..a3f5ecf8 --- /dev/null +++ b/.agents/skills/code-review/references/requesting-code-review.md @@ -0,0 +1,105 @@ +--- +name: requesting-code-review +description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements - dispatches code-reviewer subagent to review implementation against plan or requirements before proceeding +--- + +# Requesting Code Review + +Dispatch code-reviewer subagent to catch issues before they cascade. + +**Core principle:** Review early, review often. + +## When to Request Review + +**Mandatory:** +- After each task in subagent-driven development +- After completing major feature +- Before merge to main + +**Optional but valuable:** +- When stuck (fresh perspective) +- Before refactoring (baseline check) +- After fixing complex bug + +## How to Request + +**1. Get git SHAs:** +```bash +BASE_SHA=$(git rev-parse HEAD~1) # or origin/main +HEAD_SHA=$(git rev-parse HEAD) +``` + +**2. Dispatch code-reviewer subagent:** + +Use Task tool with `code-reviewer` type, fill template at `code-reviewer.md` + +**Placeholders:** +- `{WHAT_WAS_IMPLEMENTED}` - What you just built +- `{PLAN_OR_REQUIREMENTS}` - What it should do +- `{BASE_SHA}` - Starting commit +- `{HEAD_SHA}` - Ending commit +- `{DESCRIPTION}` - Brief summary + +**3. Act on feedback:** +- Fix Critical issues immediately +- Fix Important issues before proceeding +- Note Minor issues for later +- Push back if reviewer is wrong (with reasoning) + +## Example + +``` +[Just completed Task 2: Add verification function] + +You: Let me request code review before proceeding. + +BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}') +HEAD_SHA=$(git rev-parse HEAD) + +[Dispatch code-reviewer subagent] + WHAT_WAS_IMPLEMENTED: Verification and repair functions for conversation index + PLAN_OR_REQUIREMENTS: Task 2 from docs/plans/deployment-plan.md + BASE_SHA: a7981ec + HEAD_SHA: 3df7661 + DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types + +[Subagent returns]: + Strengths: Clean architecture, real tests + Issues: + Important: Missing progress indicators + Minor: Magic number (100) for reporting interval + Assessment: Ready to proceed + +You: [Fix progress indicators] +[Continue to Task 3] +``` + +## Integration with Workflows + +**Subagent-Driven Development:** +- Review after EACH task +- Catch issues before they compound +- Fix before moving to next task + +**Executing Plans:** +- Review after each batch (3 tasks) +- Get feedback, apply, continue + +**Ad-Hoc Development:** +- Review before merge +- Review when stuck + +## Red Flags + +**Never:** +- Skip review because "it's simple" +- Ignore Critical issues +- Proceed with unfixed Important issues +- Argue with valid technical feedback + +**If reviewer wrong:** +- Push back with technical reasoning +- Show code/tests that prove it works +- Request clarification + +See template at: requesting-code-review/code-reviewer.md \ No newline at end of file diff --git a/.agents/skills/code-review/references/verification-before-completion.md b/.agents/skills/code-review/references/verification-before-completion.md new file mode 100644 index 00000000..47389b35 --- /dev/null +++ b/.agents/skills/code-review/references/verification-before-completion.md @@ -0,0 +1,139 @@ +--- +name: verification-before-completion +description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always +--- + +# Verification Before Completion + +## Overview + +Claiming work is complete without verification is dishonesty, not efficiency. + +**Core principle:** Evidence before claims, always. + +**Violating the letter of this rule is violating the spirit of this rule.** + +## The Iron Law + +``` +NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE +``` + +If you haven't run the verification command in this message, you cannot claim it passes. + +## The Gate Function + +``` +BEFORE claiming any status or expressing satisfaction: + +1. IDENTIFY: What command proves this claim? +2. RUN: Execute the FULL command (fresh, complete) +3. READ: Full output, check exit code, count failures +4. VERIFY: Does output confirm the claim? + - If NO: State actual status with evidence + - If YES: State claim WITH evidence +5. ONLY THEN: Make the claim + +Skip any step = lying, not verifying +``` + +## Common Failures + +| Claim | Requires | Not Sufficient | +|-------|----------|----------------| +| Tests pass | Test command output: 0 failures | Previous run, "should pass" | +| Linter clean | Linter output: 0 errors | Partial check, extrapolation | +| Build succeeds | Build command: exit 0 | Linter passing, logs look good | +| Bug fixed | Test original symptom: passes | Code changed, assumed fixed | +| Regression test works | Red-green cycle verified | Test passes once | +| Agent completed | VCS diff shows changes | Agent reports "success" | +| Requirements met | Line-by-line checklist | Tests passing | + +## Red Flags - STOP + +- Using "should", "probably", "seems to" +- Expressing satisfaction before verification ("Great!", "Perfect!", "Done!", etc.) +- About to commit/push/PR without verification +- Trusting agent success reports +- Relying on partial verification +- Thinking "just this once" +- Tired and wanting work over +- **ANY wording implying success without having run verification** + +## Rationalization Prevention + +| Excuse | Reality | +|--------|---------| +| "Should work now" | RUN the verification | +| "I'm confident" | Confidence ≠ evidence | +| "Just this once" | No exceptions | +| "Linter passed" | Linter ≠ compiler | +| "Agent said success" | Verify independently | +| "I'm tired" | Exhaustion ≠ excuse | +| "Partial check is enough" | Partial proves nothing | +| "Different words so rule doesn't apply" | Spirit over letter | + +## Key Patterns + +**Tests:** +``` +✅ [Run test command] [See: 34/34 pass] "All tests pass" +❌ "Should pass now" / "Looks correct" +``` + +**Regression tests (TDD Red-Green):** +``` +✅ Write → Run (pass) → Revert fix → Run (MUST FAIL) → Restore → Run (pass) +❌ "I've written a regression test" (without red-green verification) +``` + +**Build:** +``` +✅ [Run build] [See: exit 0] "Build passes" +❌ "Linter passed" (linter doesn't check compilation) +``` + +**Requirements:** +``` +✅ Re-read plan → Create checklist → Verify each → Report gaps or completion +❌ "Tests pass, phase complete" +``` + +**Agent delegation:** +``` +✅ Agent reports success → Check VCS diff → Verify changes → Report actual state +❌ Trust agent report +``` + +## Why This Matters + +From 24 failure memories: +- your human partner said "I don't believe you" - trust broken +- Undefined functions shipped - would crash +- Missing requirements shipped - incomplete features +- Time wasted on false completion → redirect → rework +- Violates: "Honesty is a core value. If you lie, you'll be replaced." + +## When To Apply + +**ALWAYS before:** +- ANY variation of success/completion claims +- ANY expression of satisfaction +- ANY positive statement about work state +- Committing, PR creation, task completion +- Moving to next task +- Delegating to agents + +**Rule applies to:** +- Exact phrases +- Paraphrases and synonyms +- Implications of success +- ANY communication suggesting completion/correctness + +## The Bottom Line + +**No shortcuts for verification.** + +Run the command. Read the output. THEN claim the result. + +This is non-negotiable. \ No newline at end of file diff --git a/.agents/skills/codoo-methodology/SKILL.md b/.agents/skills/codoo-methodology/SKILL.md new file mode 100644 index 00000000..76e8552d --- /dev/null +++ b/.agents/skills/codoo-methodology/SKILL.md @@ -0,0 +1,88 @@ +--- +name: codoo-methodology +description: Use when implementing, validating, or reviewing Codoo features and Odoo instance configuration tasks for Corvanis clients, especially for FEAT specs, execution gates, evidence logs, SaaS constraints, and deterministic delivery. +--- + +# Codoo Methodology + +Reference workflow for deterministic feature delivery and Odoo configuration work in this repository. + +## When to Use + +- Building a new FEAT spec implementation +- Validating readiness of a delivered feature +- Handling SaaS constraints during addon installation +- Reviewing whether a task is complete and auditable + +## When Not to Use + +- Small exploratory tasks with no implementation or validation gates +- Purely stylistic edits that do not affect behavior + +## Core Model + +Codoo uses a specs-before-code contract with deterministic execution: + +1. Spec contract defines scope and acceptance gates. +2. Implementation happens in small, reviewable increments. +3. Validation gates prove behavior (install, API, UI, permissions). +4. Evidence logs capture pass or fail details. +5. Feature report summarizes outcomes and limitations. + +## Execution Loop (Operator View) + +1. Load feature context from `docs/features/spec-FEAT-[ID].yaml`. +2. Identify impacted zones: `src/codoo/tasks/`, `workspace/data/`, `docs/logs/`, `frontend/`, docs. +3. Implement in small increments with immediate validation per change. +4. Run mandatory gates and collect evidence artifacts. +5. Report outcome with explicit pass/fail per gate and unresolved risks. + +See detailed checklist: `references/feature-execution-checklist.md`. + +## Gate Checklist + +Use this checklist before claiming completion: + +- Install or upgrade gate passed +- API CRUD gate passed +- UI interaction gate passed +- Browser console gate passed (no relevant JS errors) +- Permissions gate passed +- Evidence files stored in `docs/logs/` +- Feature report updated in `docs/features/` + +## SaaS Limitation Handling + +If Odoo SaaS blocks custom Python behavior: + +- Record failure evidence and exact error +- Re-run up to 3 times only when a concrete fix is applied +- If still blocked, document a supported fallback: + - API-first path + - Native Odoo configuration alternative + - Explicit hard limitation with rationale + +Use the report format in `references/saas-limitation-report-template.md`. + +## Repository Guardrail + +- This workflow assumes a single repository model. +- Keep changes scoped to the impacted project area (`src/codoo/`, `frontend/`, `docs/`). +- Prefer small, reviewable commits with explicit evidence references. + +## Anti-Patterns to Avoid + +- Marking task complete without evidence files. +- Treating HTTP 405 on XML-RPC GET as outage without `authenticate()` check. +- Logging credentials or sensitive `.env` values. +- Skipping permission validation because CRUD succeeded as admin. + +## References + +- [AGENTS.md](../../../AGENTS.md) +- [docs/guides/CODOO.md](../../../docs/guides/CODOO.md) +- [docs/guides/CONTRIBUTING.md](../../../docs/guides/CONTRIBUTING.md) +- [docs/guides/ARCHITECTURE.md](../../../docs/guides/ARCHITECTURE.md) +- [docs/guides/ODOO-SAAS-LIMITATIONS.md](../../../docs/guides/ODOO-SAAS-LIMITATIONS.md) +- [references/feature-execution-checklist.md](references/feature-execution-checklist.md) +- [references/saas-limitation-report-template.md](references/saas-limitation-report-template.md) diff --git a/.agents/skills/dtg-base/CLAUDE.md b/.agents/skills/dtg-base/CLAUDE.md new file mode 100644 index 00000000..896fc0f0 --- /dev/null +++ b/.agents/skills/dtg-base/CLAUDE.md @@ -0,0 +1,127 @@ +# DTG Base Development Guide + +This file provides guidance to AI agents when working with DTG Base utilities in Odoo 18. + +## What is DTG Base? + +DTG Base is a custom abstract model (`dtg_base.DTGBase`) that provides common utility methods for Odoo development at DTG. It's inherited by other models to gain access to helpful utilities. + +## Location + +**Module**: `addons_customs/erp/dtg_base/` + +**Main Model**: `dtg_base/models/dtg_base.py` + +## When to Use DTG Base + +| Task | Method | +|------|--------| +| Get first date of month/quarter/year | `find_first_date_of_period(date, 'month')` | +| Get last date of month/quarter/year | `find_last_date_of_period(date, 'year')` | +| Convert local datetime to UTC | `convert_local_to_utc(local_dt, 'Asia/Ho_Chi_Minh')` | +| Convert UTC to local datetime | `convert_utc_to_local(utc_dt, 'Asia/Ho_Chi_Minh')` | +| Check if barcode exists | `barcode_exists('1234567890123')` | +| Generate EAN13 barcode | `get_ean13('product_code')` | +| Process large recordsets in batches | `splittor(limit=100)` | +| Remove Vietnamese accents | `strip_accents('Tiếng Việt')` | +| Zip a directory | `zip_dir(source_path, output_path)` | +| Get file size in readable format | `_get_file_size(file_path)` | + +## Inheritance Pattern + +```python +from odoo import models + +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['dtg_base.dtg_base'] + + def process_records(self): + # Use DTGBase utilities + for batch in self.splittor(limit=100): + # Process batch + pass +``` + +## Key Utilities + +### Date & Period + +```python +# Get first date of current month +first_date = self.find_first_date_of_period(fields.Date.today(), 'month') + +# Get last date of current quarter +last_date = self.find_last_date_of_period(fields.Date.today(), 'quarter') + +# Iterate over months in a period +for start, end in self.period_iter('2024-01-01', '2024-12-31', 'month'): + print(f"Period: {start} to {end}") +``` + +### Timezone Conversion + +```python +# Convert Vietnam local time to UTC +utc_dt = self.convert_local_to_utc('2024-01-15 10:00:00', 'Asia/Ho_Chi_Minh') + +# Convert UTC to Vietnam local time +local_dt = self.convert_utc_to_local(utc_dt, 'Asia/Ho_Chi_Minh') +``` + +### Batch Processing + +```python +# Process 1000 records in batches of 100 +records = self.env['my.model'].search([]) +for batch in records.splittor(limit=100): + # Process each batch + for record in batch: + # Do something + pass +``` + +### Barcode + +```python +# Check if barcode already exists +if self.barcode_exists('1234567890123'): + raise UserError("Barcode already exists!") + +# Generate EAN13 barcode +ean13 = self.get_ean13('PRODUCT123') +``` + +### Vietnamese Text + +```python +# Remove accents for search/comparison +search_text = self.strip_accents('Tiếng Việt') # -> 'Ties Viet' + +# For comparison +if self.strip_accents(record.name) == self.strip_accents(search_term): + # Match + pass +``` + +## Period Types + +| Type | Description | +|------|-------------| +| `'month'` | Month period | +| `'quarter'` | Quarter period | +| `'year'` | Year period | +| `'week'` | Week period | + +## Common Timezones + +| Timezone | UTC Offset | +|----------|------------| +| `'Asia/Ho_Chi_Minh'` | UTC+7 | +| `'UTC'` | UTC+0 | +| `'Asia/Bangkok'` | UTC+7 | +| `'Asia/Singapore'` | UTC+8 | + +--- + +**For complete reference, see [odoo-18-dtg-base-guide.md](./odoo-18-dtg-base-guide.md)** diff --git a/.agents/skills/dtg-base/README.md b/.agents/skills/dtg-base/README.md new file mode 100644 index 00000000..99197b32 --- /dev/null +++ b/.agents/skills/dtg-base/README.md @@ -0,0 +1,44 @@ +# DTG Base Skill + +Complete reference for DTG Base module utilities and helpers in Odoo 18. + +## Overview + +DTG Base is a custom abstract model that provides common utility methods for Odoo development. This skill contains comprehensive documentation for all DTGBase utilities. + +## What's Included + +- **Date & Period Utilities** - Find first/last dates, iterate over periods +- **Timezone Conversion** - Convert between local time and UTC +- **Barcode Utilities** - Validate and generate EAN13 barcodes +- **Batch Processing** - Split large recordsets into manageable batches +- **after_commit Decorator** - Execute code after transaction commit +- **Vietnamese Text** - Strip accents for search/comparison +- **File Utilities** - Zip directories, get file sizes +- **Number Utilities** - Round to specific decimal places + +## Files + +| File | Description | +|------|-------------| +| `SKILL.md` | Master index and quick reference | +| `CLAUDE.md` | AI agent guidance | +| `odoo-18-dtg-base-guide.md` | Complete DTG Base utilities reference | + +## Quick Start + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['dtg_base.dtg_base'] + + def my_method(self): + # Use DTGBase utilities + first_date = self.find_first_date_of_period('2024-01-15', 'month') + utc_date = self.convert_local_to_utc('2024-01-15 10:00:00') +``` + +## Links + +- [Full Documentation](./odoo-18-dtg-base-guide.md) +- [SKILL.md](./SKILL.md) - Quick reference diff --git a/.agents/skills/dtg-base/SKILL.md b/.agents/skills/dtg-base/SKILL.md new file mode 100644 index 00000000..ca0f6dc6 --- /dev/null +++ b/.agents/skills/dtg-base/SKILL.md @@ -0,0 +1,113 @@ +--- +name: dtg-base +description: Complete reference for DTG Base module utilities and helpers. Use when working with DTGBase inheritance, date/time utilities, timezone conversion, barcode generation/validation, batch processing, file operations, and Vietnamese text normalization. +globs: "**/addons_customs/erp/**/*.py" +license: MIT +author: UncleCat +version: 1.0.0 +--- + +# DTG Base Skill + +Complete reference for DTG Base module utilities and helpers in Odoo 18. + +## What is DTG Base? + +DTG Base is a custom abstract model (`dtg_base.DTGBase`) that provides common utility methods for Odoo development. It's designed to be inherited by other models to gain access to helpful utilities. + +## Quick Reference + +| Utility | Description | +|---------|-------------| +| [Date & Period](#date--period-utilities) | Find first/last date of period, period iteration | +| [Timezone](#timezone-conversion) | Convert local to UTC, UTC to local | +| [Barcode](#barcode-utilities) | Check barcode exists, generate EAN13 | +| [Batch Processing](#batch-processing) | Split large recordsets into batches | +| [after_commit](#after_commit-decorator) | Execute code after transaction commit | +| [Vietnamese Text](#string--text-utilities) | Strip accents, convert to non-accent | +| [File Utilities](#file-utilities) | Zip directories, get file size | +| [Number Utilities](#number-utilities) | Round to decimal places | + +--- + +## Main Guide + +**File**: `odoo-18-dtg-base-guide.md` + +### When to use this skill + +- Working with DTG Odoo codebase +- Need date/period calculations +- Timezone conversions +- Barcode validation +- Batch processing large recordsets +- Vietnamese text processing +- File zipping utilities + +--- + +## DTGBase Abstract Model + +### Inherit from DTGBase + +**Location**: `addons_customs/erp/dtg_base/models/dtg_base.py` + +```python +from odoo import models + +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['dtg_base.dtg_base'] + + def my_method(self): + # Now you have access to all DTGBase utilities + first_date = self.find_first_date_of_period('2024-01-15', 'month') + utc_date = self.convert_local_to_utc('2024-01-15 10:00:00') +``` + +--- + +## File Structure + +``` +agent-skills/skills/dtg-base/ +├── SKILL.md # This file - master index +├── odoo-18-dtg-base-guide.md # Complete DTG Base utilities reference +└── README.md # Skill overview +``` + +--- + +## Utilities Overview + +### Date & Period Utilities +- `find_first_date_of_period(date, period_type)` - Get first date of period +- `find_last_date_of_period(date, period_type)` - Get last date of period +- `period_iter(start_date, end_date, period_type)` - Iterate over periods + +### Timezone Conversion +- `convert_local_to_utc(local_dt, tz=None)` - Convert local datetime to UTC +- `convert_utc_to_local(utc_dt, tz=None)` - Convert UTC datetime to local + +### Barcode Utilities +- `barcode_exists(barcode, exclude_id=0)` - Check if barcode already exists +- `get_ean13 barcode)` - Generate/check EAN13 barcode + +### Batch Processing +- `splittor(limit=None)` - Split recordset into batches for processing + +### String & Text Utilities +- `strip_accents(text)` - Remove Vietnamese accents +- `_no_accent_vietnamese(text)` - Convert Vietnamese text + +### File Utilities +- `zip_dir(source_dir, output_file)` - Zip a directory +- `zip_dirs(dirs, output_file)` - Zip multiple directories +- `_get_file_size(file_path)` - Get human-readable file size + +### Number Utilities +- `round_decimal(value, decimal_places)` - Round to specific decimal places + +--- + +**For detailed documentation, see [odoo-18-dtg-base-guide.md](./odoo-18-dtg-base-guide.md)** diff --git a/.agents/skills/dtg-base/odoo-18-dtg-base-guide.md b/.agents/skills/dtg-base/odoo-18-dtg-base-guide.md new file mode 100644 index 00000000..f8cbbf92 --- /dev/null +++ b/.agents/skills/dtg-base/odoo-18-dtg-base-guide.md @@ -0,0 +1,778 @@ +--- +name: odoo-18-dtg-base +description: Complete reference for DTG Base module utilities and helpers. DTGBase is an abstract model providing common utility methods for date/time handling, barcode generation, timezone conversion, file operations, and more. +globs: "**/addons_customs/erp/**/*.py" +topics: + - DTGBase abstract model inheritance + - Date/Period utilities (find_first_date_of_period, find_last_date_of_period, period_iter) + - Timezone conversion (convert_local_to_utc, convert_utc_to_local) + - Barcode utilities (barcode_exists, get_ean13) + - Batch processing (splittor) + - after_commit decorator + - Vietnamese text utilities (strip_accents, _no_accent_vietnamese) + - File utilities (zip_dir, zip_dirs, _get_file_size) +when_to_use: + - Working with DTG Odoo codebase + - Need date/period calculations + - Timezone conversions + - Barcode validation + - Batch processing large recordsets + - Vietnamese text processing +--- + +# Odoo 18 DTG Base Guide + +Complete reference for DTG Base module utilities and helpers. + +## Table of Contents + +1. [DTGBase Abstract Model](#dtgbase-abstract-model) +2. [Date & Period Utilities](#date--period-utilities) +3. [Timezone Conversion](#timezone-conversion) +4. [Barcode Utilities](#barcode-utilities) +5. [Batch Processing](#batch-processing) +6. [after_commit Decorator](#after_commit-decorator) +7. [String & Text Utilities](#string--text-utilities) +8. [File Utilities](#file-utilities) +9. [Number Utilities](#number-utilities) + +--- + +## DTGBase Abstract Model + +### Inherit from DTGBase + +**Location**: `addons_customs/erp/dtg_base/models/dtg_base.py` + +```python +from odoo import models, fields + +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['dtg.base'] # Inherit DTGBase to access all utilities + + name = fields.Char() +``` + +**When to inherit**: +- Need date/period calculation utilities +- Need timezone conversion +- Need barcode validation/generation +- Need batch processing with memory management +- Need Vietnamese text processing +- Need file zipping utilities + +--- + +## Date & Period Utilities + +### Period Names + +Supported periods: `'hourly'`, `'daily'`, `'weekly'`, `'monthly'`, `'quarterly'`, `'biannually'`, `'annually'` + +Aliases also work: `'hour'`, `'day'`, `'week'`, `'month'`, `'quarter'`, `'biannual'`, `'year'`, `'annual'` + +### find_first_date_of_period() + +Find the first date of a period from any date within that period. + +```python +# Get first day of month from any date +date = fields.Date.to_date('2024-02-15') +first_day = self.find_first_date_of_period('monthly', date) +# Result: datetime(2024, 2, 1, 0, 0, 0) + +# Get first day of week (Monday) +first_week_day = self.find_first_date_of_period('weekly', date) +# Result: datetime(2024, 2, 12, 0, 0, 0) - Monday of that week + +# Get first day of quarter +first_quarter_day = self.find_first_date_of_period('quarterly', date) +# Result: datetime(2024, 1, 1, 0, 0, 0) - Q1 starts Jan 1 + +# With offset - start from 5th day +first_day_offset = self.find_first_date_of_period('monthly', date, start_day_offset=5) +# Result: datetime(2024, 2, 6, 0, 0, 0) +``` + +### find_last_date_of_period() + +Find the last date of a period from any date within that period. + +```python +# Get last day of month +date = fields.Date.to_date('2024-02-15') +last_day = self.find_last_date_of_period('monthly', date) +# Result: datetime(2024, 2, 29, 23, 59, 59, 999999) - 2024 is leap year + +# Get last day of quarter +last_quarter_day = self.find_last_date_of_period('quarterly', date) +# Result: datetime(2024, 3, 31, 23, 59, 59, 999999) + +# When given_date is also the start date +start_date = fields.Date.to_date('2024-02-01') +last_day_from_start = self.find_last_date_of_period('monthly', start_date, date_is_start_date=True) +# Result: datetime(2024, 2, 29, 23, 59, 59, 999999) + +# Custom cycle value - 2 months +last_day_2months = self.find_last_date_of_period('monthly', date, cycle_value=2) +# Result: datetime(2024, 3, 31, 23, 59, 59, 999999) - 2 month period +``` + +### period_iter() + +Generate sorted dates for periods between two dates. + +```python +# Get all month ends between two dates +dt_start = fields.Date.to_date('2024-01-15') +dt_end = fields.Date.to_date('2024-06-20') + +period_dates = self.period_iter('monthly', dt_start, dt_end) +# Result: [ +# date(2024, 1, 15), # start date +# date(2024, 1, 31), # end of Jan +# date(2024, 2, 29), # end of Feb +# date(2024, 3, 31), # end of Mar +# date(2024, 4, 30), # end of Apr +# date(2024, 5, 31), # end of May +# date(2024, 6, 20), # end date +# ] + +# Quarterly with offset +quarterly_dates = self.period_iter('quarterly', dt_start, dt_end, start_day_offset=5) +# Result includes dates starting from 5th day of each quarter +``` + +### Date Difference Utilities + +```python +# Days between dates +days = self.get_days_between_dates(date_from, date_to) + +# Hours between datetimes +hours = self.get_hours_between_dates(datetime_from, datetime_to) + +# Weeks between dates +weeks = self.get_weeks_between_dates(date_from, date_to) + +# Months between dates (float, respects odd/even months) +months = self.get_months_between_dates(date_from, date_to) +# Example: Jan 15 to Feb 14 = 0.9677 months (31 days in Jan) + +# Years between dates (float, respects leap years) +years = self.get_number_of_years_between_dates(date_from, date_to) + +# Days in month +days_in_month = self.get_days_of_month_from_date(date) + +# Day of year (1-366) +day_of_year = self.get_day_of_year_from_date(date) +# Example: Jan 6 returns 6 + +# Days in year (365 or 366) +days_in_year = self.get_days_in_year(date) +``` + +### Other Date Utilities + +```python +# Split date into components +year, month, day = self.split_date(date) + +# Next weekday +next_monday = self.next_weekday(date, weekday=0) # 0=Monday, 6=Sunday +same_weekday = self.next_weekday(date) # Same weekday next week + +# Break time range at midnight +# 2024-02-02 20:00 to 2024-02-03 04:00 +# -> [2024-02-02 20:00, 2024-02-03 00:00, 2024-02-03 04:00] +intervals = self.break_timerange_for_midnight(start_dt, end_dt) +``` + +### Period Ratio Calculation + +```python +# Calculate ratio between two periods +# Example: monthly vs daily on Feb 2024 (29 days) +ratio = self.get_ratio_between_periods('monthly', 1, 'daily', 1, given_date=date(2024, 2, 1)) +# Result: 29/7 + +# Example: quarterly vs monthly +ratio = self.get_ratio_between_periods('quarterly', 1, 'monthly', 1) +# Result: 3.0 +``` + +--- + +## Timezone Conversion + +### get_company_tz() + +Get company timezone. + +```python +# Get current company's timezone +tz = self.get_company_tz() +# Returns: 'Asia/Ho_Chi_Minh' or 'UTC' or company's timezone + +# Get specific company's timezone +tz = self.get_company_tz(company=company_record) +``` + +### convert_local_to_utc() + +Convert local datetime to UTC. + +```python +# Convert local datetime to UTC +local_dt = datetime(2024, 2, 15, 14, 30, 0) +utc_dt = self.convert_local_to_utc(local_dt, force_local_tz_name='Asia/Ho_Chi_Minh') +# Result: datetime(2024, 2, 15, 7, 30, 0) (UTC is 7 hours behind) + +# Use context tz or user tz +utc_dt = self.convert_local_to_utc(local_dt) + +# With naive=True (no timezone info in result) +utc_dt_naive = self.convert_local_to_utc(local_dt, naive=True) +# Result: datetime(2024, 2, 15, 7, 30, 0) without tzinfo + +# Convert date to datetime then to UTC +date_only = date(2024, 2, 15) +utc_from_date = self.convert_local_to_utc(date_only) +``` + +### convert_utc_to_local() + +Convert UTC datetime to local timezone. + +```python +# Convert UTC to local +utc_dt = datetime(2024, 2, 15, 7, 30, 0) +local_dt = self.convert_utc_to_local(utc_dt, force_local_tz_name='Asia/Ho_Chi_Minh') +# Result: datetime(2024, 2, 15, 14, 30, 0) + +# With DST handling +local_dt = self.convert_utc_to_local(utc_dt, is_dst=False) +``` + +### Time Conversion Utilities + +```python +# Convert datetime to float hours +# datetime(2024, 1, 1, 14, 30, 0) -> 14.5 +float_hours = self.time_to_float_hour(datetime) + +# Convert float hours to time +# 14.5 -> time(14, 30, 0) +time_obj = self.float_hours_to_time(14.5) + +# Convert hours to string "HH:MM" +time_str = self.hours_time_string(14.5) # "14:30" +time_str = self.hours_time_string(8.5) # "08:30" + +# Convert date to datetime (combines with current time) +dt = self.date_to_datetime(date_value) +``` + +--- + +## Barcode Utilities + +### barcode_exists() + +Check if barcode exists in a model. + +```python +# Check in current model +exists = self.barcode_exists('8901234567890') + +# Check in specific model +exists = self.barcode_exists('8901234567890', model_name='product.product') + +# Check with custom barcode field +exists = self.barcode_exists('8901234567890', barcode_field='default_code') + +# Check only active records (default) +exists = self.barcode_exists('8901234567890', inactive_rec=True) + +# Check all records including inactive +exists = self.barcode_exists('8901234567890', inactive_rec=False) +``` + +### get_ean13() + +Generate EAN-13 barcode checksum. + +```python +# Generate EAN-13 from 12-digit base +barcode = self.get_ean13('123456789012') +# Result: '1234567890128' (last digit is checksum) + +# Pads with zeros if less than 12 digits +barcode = self.get_ean13('123') +# Result: '000000000123X' (padded to 12 digits + checksum) +``` + +--- + +## Batch Processing + +### splittor() + +Split large recordsets into batches to avoid memory issues. + +```python +# Basic usage - splits into batches of PREFETCH_MAX (1000) +for batch in self.splittor(large_recordset): + # Process batch + batch.compute_expensive_field() + +# Custom batch size +for batch in self.splittor(large_recordset, max_rec_in_batch=500): + # Process 500 records at a time + batch.write({'field': value}) + +# Maintain order - high priority items first +for batch in self.splittor(recordset, max_rec_in_batch=100, maintain_order=True): + # Batches maintain relative order + batch.process() + +# No flush - keep in cache +for batch in self.splittor(recordset, flush=False): + # Records stay in cache + batch.read_only_operation() +``` + +**Key features**: +- Automatically divides collection into equal-sized batches +- Invalidates recordset after each batch (default) to free memory +- Set `flush=False` to keep records in cache +- Use `maintain_order=True` to preserve order across batches + +--- + +## after_commit Decorator + +Execute tasks after database transaction commits. + +```python +from odoo.addons.dtg_base.models.dtg_base import after_commit + +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['dtg.base'] + + @after_commit + def send_notification_after_commit(self): + """Send notification ONLY after transaction commits""" + for rec in self: + rec.message_post( + body=_("Record created successfully"), + message_type='notification' + ) + + def action_process(self): + # This will be called after commit + self.send_notification_after_commit() + return {'type': 'ir.actions.act_window_close'} +``` + +**Important**: +- Function runs AFTER commit, in a new cursor +- Use for notifications, external API calls, emails +- If the function raises an exception, it's logged but doesn't rollback the transaction + +--- + +## String & Text Utilities + +### strip_accents() & _no_accent_vietnamese() + +Remove accents from Vietnamese text. + +```python +# Strip accents (general + Vietnamese specific) +text = "Tiếng Việt có dấu" +no_accent = self.strip_accents(text) +# Result: "Tieng Viet khong dau" + +# Direct Vietnamese conversion +vietnamese = "Xin chào, Đất Việt nước đẹp" +converted = self._no_accent_vietnamese(vietnamese) +# Result: "Xin chao, Dat Viet nuoc dep" +``` + +--- + +## File Utilities + +### zip_dir() + +Zip a directory into bytes for storage in Binary field. + +```python +# Zip a directory +path = '/path/to/directory' +zipped_bytes = self.zip_dir(path, incl_dir=False) + +# Store in binary field +self.attachment_data = zipped_bytes + +# Include directory name in zip +zipped_with_dir = self.zip_dir(path, incl_dir=True) +``` + +### zip_dirs() + +Zip multiple directories into one archive. + +```python +# Zip multiple directories +paths = ['/path/to/dir1', '/path/to/dir2'] +zipped_bytes = self.zip_dirs(paths) + +# Store in attachment +attachment = self.env['ir.attachment'].create({ + 'name': 'archives.zip', + 'res_id': self.id, + 'res_model': self._name, + 'datas': zipped_bytes, +}) +``` + +### _get_file_size() + +Get size of file or directory. + +```python +# Get file size +file_size = self._get_file_size('/path/to/file.pdf') +# Returns: size in bytes + +# Get directory size (recursive) +dir_size = self._get_file_size('/path/to/directory') +# Returns: total size in bytes (excluding symbolic links) +``` + +--- + +## Number Utilities + +### sum_digits() + +Sum digits until result has specified number of digits. + +```python +# Sum all digits once +result = self.sum_digits(178) +# Result: 16 (1 + 7 + 8) + +# Sum until single digit +result = self.sum_digits(178, number_of_digit_return=1) +# Result: 7 (1 + 6 = 7) + +# Sum until 2 digits +result = self.sum_digits(9999, number_of_digit_return=2) +# Result: 36 (9 + 9 + 9 + 9 = 36) +``` + +### find_nearest_lucky_number() + +Find nearest number where digit sum equals 9. + +```python +# Find nearest lucky number +lucky = self.find_nearest_lucky_number(178) +# Result: 171 (1 + 7 + 1 = 9) + +# With rounding +lucky = self.find_nearest_lucky_number(178999, rounding=2) +# Result: 178900 (then adjusted to nearest lucky number) + +# Round up +lucky = self.find_nearest_lucky_number(100, round_up=True) +# Result: 108 (1 + 0 + 8 = 9) +``` + +### calculate_weights() + +Calculate weight percentages. + +```python +# Calculate weights as percentages +weights = self.calculate_weights(2, 6) +# Result: [0.25, 0.75] (25%, 75%) + +# With precision +weights = self.calculate_weights(2, 6, precision_digits=2) +# Result: [0.25, 0.75] + +# Ensure sum equals 1 +assert sum(weights) == 1.0 +``` + +### fibonacci() + +Generate Fibonacci sequence. + +```python +# Generate 5 terms +fib = self.fibonacci(5) +# Result: [0, 1, 1, 2, 3] + +# Deduplicate first 1 +fib = self.fibonacci(5, deduplicate_1=True) +# Result: [0, 1, 2, 3] - removed duplicate 1 +``` + +--- + +## Other Utilities + +### validate_year() + +Validate and convert year to integer. + +```python +# Valid year +year = self.validate_year('2024') # Returns: 2024 +year = self.validate_year(2024) # Returns: 2024 + +# Invalid year - raises ValidationError +year = self.validate_year('abc') # Raises ValidationError +year = self.validate_year(0) # Raises ValidationError +year = self.validate_year(10000) # Raises ValidationError +``` + +### identical_images() + +Compare two Image fields. + +```python +# Compare two images +is_same = self.identical_images(img1_field, img2_field) +# Returns: True if identical, False otherwise + +# Note: Does not support SVG format (PIL limitation) +``` + +### Unit Conversion + +```python +# Miles to kilometers +km = self.mile2km(10) # Returns: 16.09344 + +# Kilometers to miles +miles = self.km2mile(16) # Returns: 9.9419 +``` + +### Week Utilities + +```python +# Get weekdays for a period (max 7 days) +weekdays = self.get_weekdays_for_period(date_from, date_to) +# Returns: {0: date, 1: date, ...} where 0=Monday, 6=Sunday +``` + +--- + +## Common Patterns + +### Pattern 1: Date Range by Period + +```python +def _get_period_dates(self, date_from, date_to): + """Get all month-end dates in range""" + return self.period_iter('monthly', date_from, date_to) + +def action_report_by_period(self): + date_from = fields.Date.to_date(self.env.context.get('date_from')) + date_to = fields.Date.to_date(self.env.context.get('date_to')) + + # Get all period boundaries + period_dates = self._get_period_dates(date_from, date_to) + + for i in range(len(period_dates) - 1): + period_start = period_dates[i] + period_end = period_dates[i + 1] + # Process each period + self._process_period(period_start, period_end) +``` + +### Pattern 2: Safe Timezone Conversion + +```python +def action_schedule_meeting(self): + # Get user's local timezone + tz = self.get_company_tz() + + # Convert user input (local) to UTC for storage + utc_dt = self.convert_local_to_utc( + self.meeting_date, + force_local_tz_name=tz + ) + self.meeting_date_utc = utc_dt + + # Convert back to local for display + local_dt = self.convert_utc_to_local( + self.meeting_date_utc, + force_local_tz_name=tz + ) + self.meeting_date_display = local_dt +``` + +### Pattern 3: Batch Processing Large Recordsets + +```python +def action_recompute_all(self): + # Get all records + records = self.search([]) + + # Process in batches to avoid memory issues + for batch in self.splittor(records, max_rec_in_batch=500): + # Each batch is automatically invalidated after processing + for rec in batch: + rec._compute_expensive_field() +``` + +### Pattern 4: After-Commit Notification + +```python +@after_commit +def _send_external_notification(self): + """Send to external API after commit""" + for rec in self: + requests.post( + 'https://api.example.com/notify', + json={'record_id': rec.id, 'state': rec.state} + ) + +def action_confirm(self): + self.state = 'confirmed' + # Notification only sent if transaction commits + self._send_external_notification() +``` + +### Pattern 5: Barcode Validation + +```python +def _check_barcode_unique(self, barcode): + """Validate barcode doesn't exist""" + if self.barcode_exists(barcode): + raise UserError(_("Barcode %s already exists") % barcode) + +def create(self, vals): + if vals.get('barcode'): + self._check_barcode_unique(vals['barcode']) + return super().create(vals) +``` + +--- + +## Anti-Patterns + +| Anti-Pattern | Why Bad | Correct Approach | +|--------------|---------|------------------| +| Manual date calculation for periods | Error-prone, timezone issues | Use `find_first_date_of_period()`, `find_last_date_of_period()` | +| Processing all records at once | Memory issues with large datasets | Use `splittor()` for batch processing | +| Sending notifications before commit | Sent even if transaction rolls back | Use `@after_commit` decorator | +| Manual timezone conversion | DST issues, error-prone | Use `convert_local_to_utc()`, `convert_utc_to_local()` | +| Checking barcode with search() | Doesn't check inactive records | Use `barcode_exists()` | + +--- + +## Method Reference + +### Date/Period Methods + +| Method | Description | +|--------|-------------| +| `find_first_date_of_period(period, date, offset)` | Get first date of period | +| `find_last_date_of_period(period, date, is_start, cycle)` | Get last date of period | +| `period_iter(period, dt_start, dt_end, offset, cycle)` | Get all period dates in range | +| `get_days_between_dates(dt_from, dt_to)` | Days between dates | +| `get_months_between_dates(dt_from, dt_to)` | Months between (float) | +| `get_number_of_years_between_dates(dt_from, dt_to)` | Years between (float) | +| `get_hours_between_dates(dt_from, dt_to)` | Hours between datetimes | +| `get_days_of_month_from_date(dt)` | Number of days in month | +| `get_day_of_year_from_date(dt)` | Day of year (1-366) | +| `get_days_in_year(dt)` | Days in year (365 or 366) | +| `split_date(date)` | Split into year, month, day | +| `next_weekday(date, weekday)` | Get date next week | +| `break_timerange_for_midnight(start, end)` | Split at midnight | +| `get_ratio_between_periods(p1, d1, p2, d2, date)` | Ratio between periods | + +### Timezone Methods + +| Method | Description | +|--------|-------------| +| `get_company_tz(company)` | Get company timezone | +| `convert_local_to_utc(dt, tz, is_dst, naive)` | Local to UTC | +| `convert_utc_to_local(utc_dt, tz, is_dst, naive)` | UTC to local | +| `time_to_float_hour(dt)` | Datetime to float hours | +| `float_hours_to_time(hours, tz)` | Float to time | +| `hours_time_string(hours)` | Hours to "HH:MM" string | +| `date_to_datetime(date)` | Date to datetime | + +### Barcode Methods + +| Method | Description | +|--------|-------------| +| `barcode_exists(barcode, model, field, active)` | Check if barcode exists | +| `get_ean13(base_number)` | Generate EAN-13 checksum | + +### Batch Methods + +| Method | Description | +|--------|-------------| +| `splittor(collection, max, order, flush)` | Split into batches | + +### String Methods + +| Method | Description | +|--------|-------------| +| `strip_accents(s)` | Remove all accents | +| `_no_accent_vietnamese(s)` | Vietnamese accent removal | + +### File Methods + +| Method | Description | +|--------|-------------| +| `zip_dir(path, incl_dir)` | Zip directory | +| `zip_dirs(paths)` | Zip multiple directories | +| `_get_file_size(path)` | Get file/dir size | + +### Number Methods + +| Method | Description | +|--------|-------------| +| `sum_digits(n, digits)` | Sum digits | +| `find_nearest_lucky_number(n, round, up)` | Find lucky number | +| `calculate_weights(*weights, ...)` | Calculate percentages | +| `fibonacci(n, dedup)` | Fibonacci sequence | + +### Other Methods + +| Method | Description | +|--------|-------------| +| `validate_year(year)` | Validate year (1-9999) | +| `identical_images(img1, img2)` | Compare images | +| `mile2km(miles)` | Convert to km | +| `km2mile(km)` | Convert to miles | +| `get_weekdays_for_period(from, to)` | Get weekdays dict | + +--- + +## Module Info + +**Module**: `dtg_base` +**Version**: 1.0.0 +**Author**: AnhBT +**Location**: `addons_customs/erp/dtg_base/` +**License**: OPL-1 + +**Dependencies**: `base` + +**Files**: +- `models/dtg_base.py` - DTGBase abstract model with all utilities diff --git a/.agents/skills/odoo-19/AGENTS.md b/.agents/skills/odoo-19/AGENTS.md new file mode 100644 index 00000000..a2d71bd1 --- /dev/null +++ b/.agents/skills/odoo-19/AGENTS.md @@ -0,0 +1,173 @@ +# Odoo 19 Documentation - AI Agents Setup + +Setup guide for using Odoo 19 documentation with AI coding assistants (Cursor, Claude Code, Windsurf, Aider, etc.). + +## Quick Start + +### Install via skills.sh (Recommended) + +```bash +# Add Odoo 19 skill to your project +npx skills add unclecatvn/agent-skills +``` + +Visit [https://skills.sh/](https://skills.sh/) for more installation options. + +### Cursor IDE - Remote Rule + +Configure once in Cursor settings: +- `Settings` → `Rules` → `Add Remote Rule` +- Source: `Git Repository` +- URL: `git@github.com:unclecatvn/agent-skills.git` +- Branch: `19.0` +- Subfolder: `skills/odoo-19.0/` + +--- + +## Documentation Structure + +``` +skills/odoo-19.0/ +├── SKILL.md # Master index (all agents) +├── references/ # Development guides (18 files) +│ ├── odoo-19-actions-guide.md # ir.actions.*, cron, bindings +│ ├── odoo-19-controller-guide.md # HTTP, routing, controllers +│ ├── odoo-19-data-guide.md # XML/CSV data files, records +│ ├── odoo-19-decorator-guide.md # @api decorators +│ ├── odoo-19-development-guide.md # Manifest, wizards (overview) +│ ├── odoo-19-field-guide.md # Field types, parameters +│ ├── odoo-19-manifest-guide.md # __manifest__.py reference +│ ├── odoo-19-mixins-guide.md # mail.thread, activities, etc. +│ ├── odoo-19-model-guide.md # ORM, CRUD, search, domain +│ ├── odoo-19-migration-guide.md # Migration scripts, hooks +│ ├── odoo-19-owl-guide.md # OWL components, services +│ ├── odoo-19-performance-guide.md # N+1 prevention, optimization +│ ├── odoo-19-reports-guide.md # QWeb reports, PDF/HTML +│ ├── odoo-19-security-guide.md # ACL, record rules, security +│ ├── odoo-19-testing-guide.md # Test classes, decorators +│ ├── odoo-19-transaction-guide.md # Savepoints, errors +│ ├── odoo-19-translation-guide.md # Translations, i18n +│ └── odoo-19-view-guide.md # XML views, QWeb +├── CLAUDE.md # Claude Code specific +└── AGENTS.md # THIS FILE - setup guide +``` + +--- + +## Guide Reference + +| File | Purpose | When to Use | +|------|---------|-------------| +| `SKILL.md` | Master index for all guides | Find the right guide for your task | +| `references/odoo-19-actions-guide.md` | Actions (window, URL, server, cron) | Creating actions, menus, scheduled jobs | +| `references/odoo-19-controller-guide.md` | HTTP controllers, routing | Writing endpoints | +| `references/odoo-19-data-guide.md` | XML/CSV data files, records | Creating data files | +| `references/odoo-19-decorator-guide.md` | @api decorators usage | Using @api decorators | +| `references/odoo-19-development-guide.md` | Module structure, wizards | Creating new modules | +| `references/odoo-19-field-guide.md` | Field types, parameters | Defining model fields | +| `references/odoo-19-manifest-guide.md` | __manifest__.py reference | Configuring module manifest | +| `references/odoo-19-mixins-guide.md` | mail.thread, activities, mixins | Adding messaging, activities | +| `references/odoo-19-model-guide.md` | ORM methods, CRUD, domains | Writing model methods | +| `references/odoo-19-migration-guide.md` | Migration scripts, hooks | Upgrading modules | +| `references/odoo-19-owl-guide.md` | OWL components, hooks, services | Building OWL UI | +| `references/odoo-19-performance-guide.md` | Performance optimization | Fixing slow code | +| `references/odoo-19-reports-guide.md` | QWeb reports, templates | Creating reports | +| `references/odoo-19-security-guide.md` | ACL, record rules, security | Configuring security | +| `references/odoo-19-testing-guide.md` | Test classes, decorators, mocking | Writing tests | +| `references/odoo-19-transaction-guide.md` | Database transactions, error handling | Savepoints, UniqueViolation | +| `references/odoo-19-translation-guide.md` | Translations, localization, i18n | Adding translations | +| `references/odoo-19-view-guide.md` | XML views, actions, menus | Writing view XML | + +--- + +## AI Agent Configuration + +### Cursor IDE + +| Setting | Value | +|---------|-------| +| Source | Git Repository | +| URL | `git@github.com:unclecatvn/agent-skills.git` | +| Branch | `19.0` | +| Subfolder | `skills/odoo-19.0/` | + +**Globs patterns used by Cursor:** + +| File | globs Pattern | +|------|---------------| +| `SKILL.md` | `**/*.{py,xml}` | +| `references/odoo-19-actions-guide.md` | `**/*.{py,xml}` | +| `references/odoo-19-controller-guide.md` | `**/controllers/**/*.py` | +| `references/odoo-19-data-guide.md` | `**/*.{xml,csv}` | +| `references/odoo-19-decorator-guide.md` | `**/models/**/*.py` | +| `references/odoo-19-development-guide.md` | `**/*.{py,xml,csv}` | +| `references/odoo-19-field-guide.md` | `**/models/**/*.py` | +| `references/odoo-19-manifest-guide.md` | `**/__manifest__.py` | +| `references/odoo-19-mixins-guide.md` | `**/models/**/*.py` | +| `references/odoo-19-model-guide.md` | `**/models/**/*.py` | +| `references/odoo-19-migration-guide.md` | `**/migrations/**/*.py` | +| `references/odoo-19-owl-guide.md` | `static/src/**/*.{js,xml}` | +| `references/odoo-19-performance-guide.md` | `**/*.{py,xml}` | +| `references/odoo-19-reports-guide.md` | `**/report/**/*.xml` | +| `references/odoo-19-security-guide.md` | `**/security/**/*.{csv,xml}` | +| `references/odoo-19-testing-guide.md` | `**/tests/**/*.py` | +| `references/odoo-19-transaction-guide.md` | `**/models/**/*.py` | +| `references/odoo-19-translation-guide.md` | `**/*.{py,js,xml}` | +| `references/odoo-19-view-guide.md` | `**/views/**/*.xml` | + +### Claude Code + +```bash +# Install via skills.sh +npx skills add unclecatvn/agent-skills +``` + +Claude Code reads: +- `CLAUDE.md` - Project overview and quick reference +- `SKILL.md` - Master index for all guides +- Individual guides in `references/` - Detailed information + +### Other Agents + +| Agent | Setup | +|-------|-------| +| Windsurf | Same as Cursor (uses `.mdc` files) | +| Continue | Place `CLAUDE.md` or `SKILL.md` in root | +| Aider | Place `CLAUDE.md` or add to prompt | +| OpenCode | Copy skill folder to project - no additional config needed | + +--- + +## Cursor / Claude Skills Folder + +After installing via `npx skills add unclecatvn/agent-skills`, the skill is placed at: + +``` +.cursor/skills/ +└── odoo-19/ + └── SKILL.md + +.claude/skills/ +└── odoo-19/ + └── SKILL.md +``` + +--- + +## Key Odoo 19 Changes + +| Change | Old | New | +|--------|-----|-----| +| List view tag | `` | `` | +| Dynamic attributes | `attrs="{'invisible': [...]}"` | `invisible="..."` | +| Delete validation | Override `unlink()` | `@api.ondelete(at_uninstall=False)` | +| Field aggregation | `group_operator=` | `aggregator=` | +| SQL queries | `cr.execute()` | `SQL` class with `execute_query_dict()` | + +--- + +## Repository + +**URL**: `git@github.com:unclecatvn/agent-skills.git` +**Branch**: `19.0` +**License**: MIT diff --git a/.agents/skills/odoo-19/CLAUDE.md b/.agents/skills/odoo-19/CLAUDE.md new file mode 100644 index 00000000..3177c529 --- /dev/null +++ b/.agents/skills/odoo-19/CLAUDE.md @@ -0,0 +1,192 @@ +# Odoo 19 Development Guide + +This file provides guidance to AI agents when working with Odoo 19 code in this repository. + +> **For setup instructions with different AI IDEs, see [AGENTS.md](./AGENTS.md)** + +## Documentation Structure + +The `skills/odoo-19.0/references/` directory contains modular guides for Odoo 19 development: + +``` +skills/odoo-19.0/ +├── SKILL.md # Master index +├── references/ # Development guides (18 files) +│ ├── odoo-19-actions-guide.md # ir.actions.*, cron, bindings +│ ├── odoo-19-controller-guide.md # HTTP, routing, controllers +│ ├── odoo-19-data-guide.md # XML/CSV data files, records +│ ├── odoo-19-decorator-guide.md # @api decorators +│ ├── odoo-19-development-guide.md # Manifest, wizards (overview) +│ ├── odoo-19-field-guide.md # Field types, parameters +│ ├── odoo-19-manifest-guide.md # __manifest__.py reference +│ ├── odoo-19-mixins-guide.md # mail.thread, activities, etc. +│ ├── odoo-19-model-guide.md # ORM, CRUD, search, domain +│ ├── odoo-19-migration-guide.md # Migration scripts, hooks +│ ├── odoo-19-owl-guide.md # OWL components, services +│ ├── odoo-19-performance-guide.md # N+1 prevention, optimization +│ ├── odoo-19-reports-guide.md # QWeb reports, PDF/HTML +│ ├── odoo-19-security-guide.md # ACL, record rules, security +│ ├── odoo-19-testing-guide.md # Test classes, decorators +│ ├── odoo-19-transaction-guide.md # Savepoints, errors +│ ├── odoo-19-translation-guide.md # Translations, i18n +│ └── odoo-19-view-guide.md # XML views, QWeb +├── CLAUDE.md # This file +└── AGENTS.md # AI agents setup +``` + +## Which Guide to Use + +| Task | Guide | +| ------------------------------------- | ----------------------------------------- | +| Creating actions, menus, cron jobs | `references/odoo-19-actions-guide.md` | +| Creating a new module | `references/odoo-19-development-guide.md` | +| Configuring **manifest**.py | `references/odoo-19-manifest-guide.md` | +| Creating XML/CSV data files | `references/odoo-19-data-guide.md` | +| Writing ORM queries/search | `references/odoo-19-model-guide.md` | +| Defining model fields | `references/odoo-19-field-guide.md` | +| Using @api decorators | `references/odoo-19-decorator-guide.md` | +| Writing XML views | `references/odoo-19-view-guide.md` | +| Fixing slow code/N+1 queries | `references/odoo-19-performance-guide.md` | +| Handling database errors | `references/odoo-19-transaction-guide.md` | +| Creating HTTP endpoints | `references/odoo-19-controller-guide.md` | +| Building OWL components | `references/odoo-19-owl-guide.md` | +| Upgrading modules/migrating data | `references/odoo-19-migration-guide.md` | +| Using mail.thread, activities, mixins | `references/odoo-19-mixins-guide.md` | +| Creating QWeb reports | `references/odoo-19-reports-guide.md` | +| Configuring security (ACL, rules) | `references/odoo-19-security-guide.md` | +| Writing tests | `references/odoo-19-testing-guide.md` | +| Adding translations/localization | `references/odoo-19-translation-guide.md` | + +## Key Odoo 19 Changes + +| Change | Old (Odoo 17-) | New (Odoo 19) | +| ------------------ | ------------------------------ | ------------------------------------------ | +| List view tag | `` | `` | +| Dynamic attributes | `attrs="{'invisible': [...]}"` | `invisible="..."` (direct) | +| Delete validation | Override `unlink()` | `@api.ondelete(at_uninstall=False)` | +| Field aggregation | `group_operator=` | `aggregator=` | +| SQL queries | `cr.execute()` | `SQL` class with `execute_query_dict()` | +| Batch create | Single dict | List of dicts (`create([{...}, {...}])`) | +| SQL constraints | `_sql_constraints = [...]` | `models.Constraint(...)` | +| DB indexes | `index=True` only | `models.Index(...)` declarative | +| Kanban template | `t-name="kanban-box"` | `t-name="card"` | +| QWeb output | `t-esc` | `t-out` (t-esc deprecated) | +| Security groups | `category_id` on `res.groups` | `privilege_id` + `res.groups.privilege` | +| Private methods | `_` prefix convention | `@api.private` decorator (enforced) | +| Model naming | `_name = 'res.users'` required | CamelCase class → auto-derive `_name` | +| read_group | `read_group()` | `_read_group()` / `formatted_read_group()` | + +## Critical Anti-Patterns + +| Anti-Pattern | Why Bad | Correct Approach | +| -------------------------------------------------------------- | ---------------------------- | -------------------------------------------------- | +| `attrs="{'invisible': [...]}"` | Deprecated in Odoo 18 | Use `invisible="..."` direct attribute | +| `@api.depends('partner_id')` then accessing `partner_id.email` | N queries per record | Add `@api.depends('partner_id.email')` | +| `search()` inside loop | N+1 queries | Use `search()` with `IN` domain or `_read_group()` | +| `create()` in loop | N INSERT statements | Batch: `create([{...}, {...}])` | +| Overriding `unlink()` for validation | Breaks module uninstall | Use `@api.ondelete(at_uninstall=False)` | +| Using `` in Odoo 19 | Deprecated tag | Use `` instead | +| Using `_sql_constraints` | **Not supported in Odoo 19** | Use `models.Constraint(...)` | +| Using `t-esc` in templates | Deprecated directive | Use `t-out` instead | +| Using `category_id` in `res.groups` | Removed in Odoo 19 | Use `privilege_id` + `res.groups.privilege` | +| Using `read_group()` | Deprecated | Use `_read_group()` or `formatted_read_group()` | + +## @api Decorator Decision Tree + +``` +Need to define field behavior? +├── Field computed from other fields → @api.depends +│ └── CAN use dotted paths: `@api.depends('partner_id.email')` +├── Validate data → @api.constrains +│ └── CANNOT use dotted paths: only simple field names +├── Prevent record deletion → @api.ondelete (Odoo 18+) +└── Update form UI → @api.onchange + └── NO CRUD operations allowed + +Need to define method behavior? +├── Method-level, doesn't depend on self → @api.model +├── Mark method as non-RPC callable → @api.private +└── Normal record method → no decorator needed +``` + +## Common Patterns Reference + +### N+1 Query Prevention + +```python +# BAD: search in loop +for order in orders: + payments = self.env['payment'].search([('order_id', '=', order.id)]) + +# GOOD: single query +payments = self.env['payment'].search_read([('order_id', 'in', orders.ids)]) +``` + +### List View (Odoo 19) + +```xml + + + + +``` + +### Delete Validation (Odoo 19) + +```python +@api.ondelete(at_uninstall=False) +def _unlink_if_not_draft(self): + if any(rec.state != 'draft' for rec in self): + raise UserError("Cannot delete non-draft records") +``` + +## Module Structure + +``` +my_module/ +├── __init__.py +├── __manifest__.py +├── models/ +│ ├── __init__.py +│ └── my_model.py +├── views/ +│ └── my_model_views.xml +├── security/ +│ ├── ir.model.access.csv +│ └── my_module_security.xml +├── data/ +│ └── my_module_data.xml +├── migrations/ +│ └── 19.0.1.0/ +│ └── post-migration.py +├── tests/ +│ ├── __init__.py +│ └── test_my_model.py +├── wizard/ +│ ├── __init__.py +│ └── my_wizard.py +├── controllers/ +│ ├── __init__.py +│ └── my_controller.py +└── static/ + └── src/ + ├── js/ + │ └── my_component.js + ├── xml/ + │ └── my_component.xml + └── scss/ + └── my_style.scss +``` + +## Base Code Reference + +The guides are based on Odoo 19 source code. Reference these files in your Odoo installation: + +- `odoo/models.py` - ORM implementation +- `odoo/fields.py` - Field types +- `odoo/api.py` - Decorators +- `odoo/http.py` - HTTP layer +- `odoo/exceptions.py` - Exception types +- `odoo/tools/translate.py` - Translation system +- `odoo/addons/base/models/res_lang.py` - Language model +- `addons/web/static/src/core/l10n/translation.js` - JS translations diff --git a/.agents/skills/odoo-19/SKILL.md b/.agents/skills/odoo-19/SKILL.md new file mode 100644 index 00000000..63a6c362 --- /dev/null +++ b/.agents/skills/odoo-19/SKILL.md @@ -0,0 +1,97 @@ +--- +name: odoo-19 +description: >- + Odoo 19 development knowledge base with 18 specialized guides covering + Actions (ir.actions.*, cron jobs, server actions), Controllers (HTTP + routing, endpoints, auth types), Data files (XML/CSV records, shortcuts, + noupdate), API Decorators (@api.depends, @api.constrains, @api.ondelete, + @api.onchange, @api.model, @api.private), SQL Constraints (models.Constraint + replacing _sql_constraints), Database Indexes (models.Index), Module + development (manifest, wizards, reports), Field types (Char, Text, Monetary, + relational fields), Manifest configuration (__manifest__.py, dependencies, + asset bundles), Mixins (mail.thread, mail.activity.mixin, mail.alias.mixin, + utm.mixin), ORM Model methods (search, CRUD, domain filters, recordsets, + CamelCase model naming), Migration scripts (pre/post/end hooks, data + migration), OWL frontend components (hooks, services, lifecycle), + Performance optimization (N+1 prevention, batch ops, _read_group), QWeb + Reports (PDF/HTML, paper formats, barcodes, t-out), Security/ACL (record + rules, field permissions, privilege-based groups, @api.private), Testing + (TransactionCase, HttpCase, mocking, query count assertions), Transactions + (savepoints, UniqueViolation, serialization failures), Translations (i18n, + PO files, translatable fields), XML Views (list/form/search, kanban card + templates, xpath inheritance, QWeb templates). Use when writing, reviewing, + or debugging any Odoo 19 Python or XML code, creating or modifying modules, + fixing performance issues, or looking up Odoo 19 API patterns and best + practices. +--- + +# Odoo 19 Skill - Master Index + +Master index for all Odoo 19 development guides. Read the appropriate guide from `references/` based on your task. + +## Quick Reference + +| Topic | File | When to Use | +| -------------- | ----------------------------------------- | ------------------------------------------------------- | +| Actions | `references/odoo-19-actions-guide.md` | Creating actions, menus, scheduled jobs, server actions | +| API Decorators | `references/odoo-19-decorator-guide.md` | Using @api decorators, compute fields, validation | +| Controllers | `references/odoo-19-controller-guide.md` | Writing HTTP endpoints, routes, web controllers | +| Data Files | `references/odoo-19-data-guide.md` | XML/CSV data files, records, shortcuts | +| Development | `references/odoo-19-development-guide.md` | Creating modules, manifest, reports, security, wizards | +| Field Types | `references/odoo-19-field-guide.md` | Defining model fields, choosing field types | +| Manifest | `references/odoo-19-manifest-guide.md` | **manifest**.py configuration, dependencies, hooks | +| Migration | `references/odoo-19-migration-guide.md` | Upgrading modules, data migration, version changes | +| Mixins | `references/odoo-19-mixins-guide.md` | mail.thread, activities, email aliases, tracking | +| Model Methods | `references/odoo-19-model-guide.md` | Writing ORM queries, CRUD operations, domain filters | +| OWL Components | `references/odoo-19-owl-guide.md` | Building OWL UI components, hooks, services | +| Performance | `references/odoo-19-performance-guide.md` | Optimizing queries, fixing slow code, preventing N+1 | +| Reports | `references/odoo-19-reports-guide.md` | QWeb reports, PDF/HTML, templates, paper formats | +| Security | `references/odoo-19-security-guide.md` | Access rights, record rules, field permissions | +| Testing | `references/odoo-19-testing-guide.md` | Writing tests, mocking, assertions, browser testing | +| Transactions | `references/odoo-19-transaction-guide.md` | Handling database errors, savepoints, UniqueViolation | +| Translation | `references/odoo-19-translation-guide.md` | Adding translations, localization, i18n | +| Views & XML | `references/odoo-19-view-guide.md` | Writing XML views, actions, menus, QWeb templates | + +## File Structure + +``` +skills/odoo-19.0/ +├── SKILL.md # This file - master index +└── references/ # Development guides + ├── odoo-19-actions-guide.md + ├── odoo-19-controller-guide.md + ├── odoo-19-data-guide.md + ├── odoo-19-decorator-guide.md + ├── odoo-19-development-guide.md + ├── odoo-19-field-guide.md + ├── odoo-19-manifest-guide.md + ├── odoo-19-migration-guide.md + ├── odoo-19-mixins-guide.md + ├── odoo-19-model-guide.md + ├── odoo-19-owl-guide.md + ├── odoo-19-performance-guide.md + ├── odoo-19-reports-guide.md + ├── odoo-19-security-guide.md + ├── odoo-19-testing-guide.md + ├── odoo-19-transaction-guide.md + ├── odoo-19-translation-guide.md + └── odoo-19-view-guide.md +``` + +## Base Code Reference (Odoo 19) + +All guides are based on analysis of Odoo 19 source code: + +- `odoo/models.py` - ORM implementation +- `odoo/fields.py` - Field types +- `odoo/api.py` - Decorators +- `odoo/http.py` - HTTP layer +- `odoo/exceptions.py` - Exception types +- `odoo/tools/translate.py` - Translation system +- `odoo/addons/base/models/res_lang.py` - Language model +- `addons/web/static/src/core/l10n/translation.js` - JS translations + +## External Documentation + +- [Odoo 19 Official Documentation](https://github.com/odoo/documentation/tree/19.0) +- [Odoo 19 Developer Reference](https://github.com/odoo/documentation/blob/19.0/developer/reference/orm.rst) diff --git a/.agents/skills/odoo-19/references/odoo-19-actions-guide.md b/.agents/skills/odoo-19/references/odoo-19-actions-guide.md new file mode 100644 index 00000000..404b0698 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-actions-guide.md @@ -0,0 +1,341 @@ +# Odoo 19 Actions Guide + +Guide for working with Odoo 19 actions (`ir.actions.*`), scheduled jobs (cron), and action bindings. + +## Table of Contents +- [Action Types](#action-types) +- [Window Actions](#window-actions) +- [Server Actions](#server-actions) +- [Report Actions](#report-actions) +- [Client Actions](#client-actions) +- [URL Actions](#url-actions) +- [Scheduled Actions (Cron)](#scheduled-actions-cron) +- [Action Bindings](#action-bindings) + +--- + +## Action Types + +Actions define the behavior of the system in response to user actions: login, action button, selection of records, etc. + +Actions can be stored in the database or returned directly as dictionaries. All actions share two mandatory attributes: + +| Attribute | Type | Description | +|-----------|------|-------------| +| `type` | string | The category of the current action | +| `name` | string | Short user-readable description | + +A client can get actions in 4 forms: +- `False` - closes any open action dialog +- A string - client action tag or number +- A number - database identifier or external ID +- A dictionary - client action descriptor + +--- + +## Window Actions + +`ir.actions.act_window` - The most common action type, used to present visualizations of a model through views. + +### Key Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `res_model` | string | Model to present views for | +| `views` | list | List of `(view_id, view_type)` pairs | +| `res_id` | int | If default view is `form`, specifies the record to load | +| `search_view_id` | tuple | `(id, name)` pair for specific search view | +| `target` | string | `current`, `fullscreen`, `new`, or `main` | +| `context` | dict | Additional context data | +| `domain` | list | Filtering domain | +| `limit` | int | Number of records to display (default: 80) | + +### Example: Opening customers + +```python +{ + "type": "ir.actions.act_window", + "res_model": "res.partner", + "views": [[False, "list"], [False, "form"]], + "domain": [["customer", "=", True]], +} +``` + +### Example: Opening specific product in dialog + +```python +{ + "type": "ir.actions.act_window", + "res_model": "product.product", + "views": [[False, "form"]], + "res_id": a_product_id, + "target": "new", +} +``` + +### In-Database Fields + +When defining actions from XML data files: + +| Attribute | Description | +|-----------|-------------| +| `view_mode` | Comma-separated list of view types (e.g., `list,form`) | +| `view_ids` | Many2many to view objects | +| `view_id` | Specific view to add to views list | + +### Using ir.actions.act_window.view + +```xml + + + list + + + +``` + +--- + +## Server Actions + +`ir.actions.server` - Allow triggering complex server code from any valid action location. + +### Key Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `id` | int | In-database identifier | +| `model_id` | ref | Odoo model linked to the action | +| `state` | string | Type of action: `code`, `object_create`, `object_write`, `multi` | +| `code` | string | Python code to execute | + +### State: code + +```xml + + Res Partner Server Action + + code + + raise Warning(record.name) + + +``` + +### Returning next action + +```xml + + Open Form Action + + code + + if record.some_condition(): + action = { + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": record._name, + "res_id": record.id, + } + + +``` + +### State: object_create + +| Attribute | Description | +|-----------|-------------| +| `crud_model_id` | Model in which to create a new record | +| `link_field_id` | Many2one field on which to set newly created record | +| `fields_lines` | Fields to override when creating | + +### State: object_write + +Updates the current record(s) following `fields_lines` specifications. + +### State: multi + +Executes several actions given through `child_ids`. + +### Evaluation Context + +Available variables in server actions: +- `model` - Model object linked to the action +- `record`/`records` - Record/recordset on which the action is triggered +- `env` - Odoo Environment +- `datetime`, `dateutil`, `time`, `timezone` - Python modules +- `log(message, level='info')` - Logging function +- `Warning` - Constructor for Warning exception + +--- + +## Report Actions + +`ir.actions.report` - Triggers the printing of a report. + +### Key Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | string | Used as file name if `print_report_name` not specified | +| `model` | string | Model your report will be about | +| `report_type` | string | `qweb-pdf` or `qweb-html` | +| `report_name` | string | External ID of the qweb template | +| `print_report_name` | string | Python expression for report name | +| `groups_id` | Many2many | Groups allowed to view/use the report | +| `multi` | boolean | If True, not displayed on form view | +| `paperformat_id` | Many2one | Paper format to use | +| `attachment_use` | boolean | Generate once, then reprint from stored report | +| `attachment` | string | Python expression for attachment name | + +### Print Menu Integration + +If you define your report through a `` and want it in the Print menu: + +```xml + + My Report + my.model + qweb-pdf + my_module.my_template + + +``` + +--- + +## Client Actions + +`ir.actions.client` - Triggers an action implemented entirely in the client. + +### Key Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `tag` | string | Client-side identifier of the action | +| `params` | dict | Additional data to send to the client | +| `target` | string | `current`, `fullscreen`, or `new` | + +```python +{ + "type": "ir.actions.client", + "tag": "pos.ui" +} +``` + +Tells the client to start the Point of Sale interface. + +--- + +## URL Actions + +`ir.actions.act_url` - Allow opening a URL (website/web page). + +### Key Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `url` | string | The address to open | +| `target` | string | `new`, `self`, or `download` | + +```python +{ + "type": "ir.actions.act_url", + "url": "https://odoo.com", + "target": "self", +} +``` + +--- + +## Scheduled Actions (Cron) + +`ir.cron` - Actions triggered automatically on a predefined frequency. + +### Key Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `name` | string | Name of the scheduled action | +| `interval_number` | int | Number of interval_type units between executions | +| `interval_type` | string | `minutes`, `hours`, `days`, `weeks`, `months` | +| `model_id` | ref | Model on which this action will be called | +| `code` | string | Code content of the action | +| `nextcall` | datetime | Next planned execution date | +| `priority` | int | Priority when executing multiple actions | + +### Writing cron functions + +When writing cron functions, batch the progress to avoid blocking workers: + +```python +def _cron_do_something(self, *, limit=300): + domain = [('state', '=', 'ready')] + records = self.search(domain, limit=limit) + records.do_something() + # notify progression + remaining = 0 if len(records) == limit else self.search_count(domain) + self.env['ir.cron']._commit_progress(len(records), remaining=remaining) +``` + +### Managing resources between batches + +```python +def _cron_do_something(self): + assert self.env.context.get('cron_id'), "Run only inside cron jobs" + domain = [('state', '=', 'ready')] + records = self.search(domain) + self.env['ir.cron']._commit_progress(remaining=len(records)) + + with open_some_connection() as conn: + for record in records: + record = record.try_lock_for_update().filtered_domain(domain) + if not record: + continue + try: + record.do_something(conn) + if not self.env['ir.cron']._commit_progress(1): + break + except Exception: + self.env.cr.rollback() +``` + +### Running cron functions + +Do not call cron functions directly. Use: +- `IrCron.method_direct_trigger()` - for testing +- `IrCron._trigger()` - for scheduled execution + +### Security Measures + +- If a scheduled action encounters an error or timeout **3 consecutive times**, it skips current execution +- If it fails **5 consecutive times** over **7 days**, it is deactivated and notifies the DB admin +- A hard-limit exists for cron execution at the database level + +--- + +## Action Bindings + +Actions can be bound to models to appear in contextual menus. + +### Binding Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `binding_model_id` | Many2one | Model the action is bound to (use `model_id` for Server Actions) | +| `binding_type` | string | `action` (default) or `report` | +| `binding_view_types` | string | Comma-separated list: `list`, `form`, or `list,form` (default) | + +### Binding Type: action + +Action appears in the **Action** contextual menu. + +### Binding Type: report + +Action appears in the **Print** contextual menu. + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/actions.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-controller-guide.md b/.agents/skills/odoo-19/references/odoo-19-controller-guide.md new file mode 100644 index 00000000..4efe7cab --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-controller-guide.md @@ -0,0 +1,344 @@ +# Odoo 19 Controller Guide + +Guide for creating HTTP controllers and routes in Odoo 19. + +## Table of Contents + +- [Controllers](#controllers) +- [Routes](#routes) +- [Request](#request) +- [Response](#response) +- [Authentication](#authentication) +- [JSON-RPC](#json-rpc) + +--- + +## Controllers + +Controllers provide extensibility similar to models, but with a separate mechanism (since database may not be available). + +Controllers are created by inheriting from `odoo.http.Controller`: + +```python +from odoo import http + +class MyController(http.Controller): + @http.route('/some_url', auth='public') + def handler(self): + return stuff() +``` + +### Controller Inheritance + +To override a controller, inherit from its class and override methods: + +```python +class Extension(MyController): + @http.route() + def handler(self): + do_before() + return super(Extension, self).handler() +``` + +**Important**: + +- Always re-apply `@http.route()` decorator to keep route visible +- Without decorator, method is "unpublished" +- Decorator arguments override previous ones + +### Change Authentication + +```python +class Restrict(MyController): + @http.route(auth='user') + def handler(self): + return super(Restrict, self).handler() +``` + +This changes `/some_url` from public to user (requires login). + +--- + +## Routes + +### Route Decorator + +`@http.route()` defines routing for controller methods. + +```python +@http.route('/hello', auth='public', website=True) +def hello(self): + return "Hello World!" +``` + +### Route Parameters + +| Parameter | Description | +| --------- | ---------------------------------------------- | +| `route` | Route path(s) (string or list) | +| `auth` | Authentication type (`public`, `user`, `none`) | +| `methods` | Allowed HTTP methods (`GET`, `POST`, etc.) | +| `type` | Response type (`http`, `json`) | +| `website` | Boolean: bind to current website | +| `csrf` | Boolean: CSRF protection (default: True) | +| `sitemap` | Boolean or sitemap config | + +### Multiple Routes + +```python +@http.route(['/hello', '/bonjour'], auth='public') +def hello_bonjour(self): + return "Hello or Bonjour!" +``` + +### HTTP Methods + +```python +@http.route('/api/data', methods=['GET'], auth='user', type='json') +def get_data(self): + return {'data': 'value'} + +@http.route('/api/data', methods=['POST'], auth='user', type='json') +def post_data(self, **kwargs): + return {'result': 'created'} +``` + +--- + +## Authentication + +### Authentication Types + +| Type | Description | +| --------- | ----------------------------- | +| `public` | No authentication required | +| `user` | Requires active user session | +| `none` | No authentication, no session | +| `website` | Public with website support | + +### Examples + +```python +# Public route +@http.route('/page', auth='public') +def public_page(self): + return "Everyone can see this" + +# User-only route +@http.route('/my-account', auth='user') +def user_page(self): + return "Only logged users can see this" + +# No authentication +@http.route('/api/status', auth='none', type='json') +def status(self): + return {'status': 'ok'} +``` + +### Current User + +```python +@http.route('/profile', auth='user') +def profile(self): + # Access current user + user = http.request.env.user + return f"Hello, {user.name}" +``` + +--- + +## Request + +The request object is automatically set on `odoo.http.request` at the start of each request. + +### Request Properties + +| Property | Description | +| ------------- | ------------------------------------ | +| `httprequest` | Original Werkzeug request | +| `env` | Odoo environment for current request | +| `db` | Current database | +| `uid` | Current user id | +| `context` | Request context | +| `session` | Session | +| `cr` | Database cursor | +| `lang` | Current language | +| `registry` | Model registry | + +### Example + +```python +@http.route('/info', auth='user') +def info(self): + request = http.request + user = request.env.user + company = request.env.company + return f"{user.name} @ {company.name}" +``` + +### Session + +```python +@http.route('/set-value', auth='public', methods=['POST']) +def set_value(self, key, value): + http.request.session[key] = value + return "OK" + +@http.route('/get-value', auth='public') +def get_value(self, key): + return http.request.session.get(key, 'not set') +``` + +--- + +## Response + +### HTTP Response + +Return string for HTML, dict for JSON: + +```python +# HTML response +@http.route('/html', auth='public', type='http') +def html_response(self): + return "

Hello

" + +# JSON response +@http.route('/json', auth='public', type='json') +def json_response(self): + return {'key': 'value'} +``` + +### Redirect + +```python +from odoo.http import redirect + +@http.route('/old-url', auth='public') +def old_url(self): + return redirect('/new-url') +``` + +### File Response + +```python +@http.route('/download', auth='user') +def download_file(self): + file_content = b'file data' + headers = [ + ('Content-Type', 'application/pdf'), + ('Content-Disposition', 'attachment; filename="file.pdf"'), + ] + return request.make_response( + file_content, + headers + ) +``` + +--- + +## JSON-RPC + +### JSON Controller + +```python +@http.route('/api/search', auth='user', type='json', methods=['POST']) +def json_search(self, model, domain, fields=None): + Model = http.request.env[model] + records = Model.search(domain) + if fields: + records = records.read(fields) + else: + records = records.read() + return {'result': records} +``` + +### Call from JavaScript + +```javascript +rpc("/api/search", { + model: "res.partner", + domain: [["is_company", "=", true]], + fields: ["name", "email"], +}).then(function (result) { + console.log(result); +}); +``` + +--- + +## Website Routes + +### Website Page + +```python +class WebsiteController(http.Controller): + @http.route('/my-page', auth='public', website=True) + def my_page(self): + return http.request.render('my_module.my_page_template', { + 'title': 'My Page', + }) +``` + +### Template + +```xml + +``` + +--- + +## Controllers Best Practices + +### Always Return a Value + +```python +# BAD: no return +@http.route('/bad', auth='public') +def bad(self): + pass + +# GOOD: return something +@http.route('/good', auth='public') +def good(self): + return "Response" +``` + +### Use Proper Authentication + +```python +# BAD: public for sensitive data +@http.route('/sensitive', auth='public') +def sensitive(self): + return secret_data() + +# GOOD: user authentication +@http.route('/sensitive', auth='user') +def sensitive(self): + return secret_data() +``` + +### CSRF Protection + +CSRF is enabled by default for POST. Disable with caution: + +```python +@http.route('/webhook', auth='public', methods=['POST'], csrf=False) +def webhook(self): + # External webhook, no CSRF token + return "OK" +``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/http.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-data-guide.md b/.agents/skills/odoo-19/references/odoo-19-data-guide.md new file mode 100644 index 00000000..ee5fcaf0 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-data-guide.md @@ -0,0 +1,298 @@ +# Odoo 19 Data Files Guide + +Guide for working with Odoo 19 data files (XML and CSV), records, and shortcuts. + +## Table of Contents + +- [XML Data Files Structure](#xml-data-files-structure) +- [Record Tag](#record-tag) +- [Field Tag](#field-tag) +- [Delete Tag](#delete-tag) +- [Function Tag](#function-tag) +- [Shortcuts](#shortcuts) +- [CSV Data Files](#csv-data-files) + +--- + +## XML Data Files Structure + +The main way to define data in Odoo is via XML data files: + +```xml + + + + ... + +``` + +### noupdate Flag + +If content should only be applied once: + +```xml + + + + + + + + + +``` + +--- + +## Record Tag + +`record` - Defines or updates a database record. + +### Attributes + +| Attribute | Type | Required | Description | +| ------------- | ------ | -------- | ---------------------------------------------------------- | +| `model` | string | Yes | Name of the model to create/update | +| `id` | string | No\* | External identifier for this record (strongly recommended) | +| `context` | dict | No | Context to use when creating | +| `forcecreate` | bool | No | In update mode, create if doesn't exist (default: True) | + +\*Required for record updates; recommended for creation + +### Example + +```xml + + Odoo + + + +``` + +--- + +## Field Tag + +Each `record` can have `field` tags defining values. + +### Attributes + +| Attribute | Type | Description | +| --------- | ------ | ----------------------------------------- | +| `name` | string | **Required**. Name of the field to set | +| `ref` | string | External ID to look up and set | +| `search` | domain | Search domain, result set as field value | +| `eval` | string | Python expression to evaluate | +| `type` | string | Interpret field content (see types below) | + +### Value Methods + +#### Nothing (False) + +```xml + +``` + +#### search + +For relational fields, evaluates a domain and sets the result: + +```xml + +``` + +Only first result used for Many2one fields. + +#### ref + +Look up an external ID: + +```xml + + +``` + +#### type + +Available types: + +| Type | Description | +| --------------- | ------------------------------------------------------ | +| `xml`, `html` | Extract children as document, evaluate external IDs | +| `file` | Ensure content is valid file path, saves `module,path` | +| `char` | Set content directly without alterations | +| `base64` | Base64-encode content (use with `file` attribute) | +| `int`, `float` | Convert to number | +| `list`, `tuple` | Contains `value` elements | + +```xml + +

Hello

+
+ + +``` + +#### eval + +Evaluate a Python expression: + +```xml + + + +``` + +Evaluation context: + +- `time`, `datetime`, `timedelta`, `relativedelta` modules +- `ref()` function to resolve external IDs +- `obj` for current field's model + +--- + +## Delete Tag + +`delete` - Removes records. + +### Attributes + +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | -------------------------------- | +| `model` | string | Yes | Model in which to delete | +| `id` | string | No\* | External ID of record to remove | +| `search` | domain | No\* | Domain to find records to remove | + +\*Exclusive: use either `id` or `search` + +### Examples + +```xml + + + + + +``` + +--- + +## Function Tag + +`function` - Calls a method on a model. + +### Attributes + +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------------------- | +| `model` | string | Yes | Model to call method on | +| `name` | string | Yes | Name of method to call | + +### Parameters + +Via `eval` (should evaluate to sequence): + +```xml + +``` + +Via `value` elements: + +```xml + + + +``` + +--- + +## Shortcuts + +Because some structural models are complex, data files provide shorter alternatives. + +### menuitem + +Defines an `ir.ui.menu` record with defaults: + +| Attribute | Description | +| --------- | ------------------------------------------------------------------------- | +| `parent` | External ID of parent menu, or interpret `name` as `/`-separated sequence | +| `name` | Menu name (or get from linked action) | +| `groups` | Comma-separated external IDs for `res.groups` (prefix `-` removes group) | +| `action` | External ID of action to execute | +| `id` | External identifier | + +```xml + + +``` + +### template + +Creates a QWeb view requiring only the `arch` section: + +| Attribute | Description | +| ------------ | ---------------------------------------------- | +| `id` | External identifier | +| `name` | View name | +| `inherit_id` | External ID of parent view | +| `priority` | View priority | +| `primary` | If True with `inherit_id`, defines as primary | +| `groups` | Comma-separated group external IDs | +| `active` | Whether view is active (for inheritance views) | + +```xml + +``` + +### asset + +Creates an `ir.asset` record: + +```xml + + web.assets_frontend + website_something/static/src/some_style.scss + +``` + +--- + +## CSV Data Files + +XML is verbose for bulk creation. CSV files are simpler for same-model records. + +### Structure + +- File name: `{model_name}.csv` +- First row: fields to write, special field `id` for external IDs +- Each row: creates a new record + +### Example: `res.country.state.csv` + +```csv +id,country_id,name,code +state_1_us,country_us,Alabama,AL +state_2_us,country_us,Alaska,AK +state_3_us,country_us,Arizona,AZ +``` + +### Notes + +- First column: external ID for creation/update +- Second column: external ID of country object to link to +- Third column: `name` field value +- Fourth column: `code` field value + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/data.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-decorator-guide.md b/.agents/skills/odoo-19/references/odoo-19-decorator-guide.md new file mode 100644 index 00000000..4b757538 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-decorator-guide.md @@ -0,0 +1,458 @@ +# Odoo 19 Decorator Guide + +Guide for using `@api` decorators in Odoo 19: computed fields, validation, onchange, and more. + +## Table of Contents + +- [Method Decorators](#method-decorators) +- [@api.depends](#apidepends) +- [@api.constrains](#apiconstrains) +- [@api.ondelete](#apiondelete) +- [@api.onchange](#apionchange) +- [@api.model](#apimodel) +- [@api.model_create_multi](#apimodel_create_multi) +- [@api.autovacuum](#apiautovacuum) +- [@api.private](#apiprivate) +- [@api.returns](#apireturns) + +--- + +## Method Decorators + +Decorators in Odoo 19 are in `odoo.api` module: + +```python +from odoo import api, models + +class MyModel(models.Model): + _name = 'my.model' + + @api.depends('field') + def _compute_method(self): + pass +``` + +--- + +## @api.depends + +For computed fields. Specifies dependencies that trigger recomputation. + +### Basic Usage + +```python +total = fields.Float(compute='_compute_total', store=True) + +@api.depends('value', 'tax') +def _compute_total(self): + for record in self: + record.total = record.value + record.value * record.tax +``` + +### Dotted Paths + +Can use dotted paths for relational fields: + +```python +@api.depends('partner_id.email') +def _compute_email(self): + for record in self: + record.email = record.partner_id.email +``` + +### Multiple Fields + +Compute multiple fields: + +```python +discount_value = fields.Float(compute='_apply_discount') +total = fields.Float(compute='_apply_discount') + +@api.depends('value', 'discount') +def _apply_discount(self): + for record in self: + discount = record.value * record.discount + record.discount_value = discount + record.total = record.value - discount +``` + +### Search on Computed Field + +```python +upper_name = fields.Char(compute='_compute_upper', search='_search_upper') + +@api.depends('name') +def _compute_upper(self): + for record in self: + record.upper_name = record.name.upper() if record.name else False + +def _search_upper(self, operator, value): + if operator == 'like': + operator = 'ilike' + return [('name', operator, value)] +``` + +### Inverse Method + +Allow setting computed field: + +```python +document = fields.Char(compute='_get_document', inverse='_set_document') + +@api.depends('document_path') +def _get_document(self): + for record in self: + with open(record.document_path) as f: + record.document = f.read() + +def _set_document(self): + for record in self: + if not record.document: + continue + with open(record.document_path) as f: + f.write(record.document) +``` + +--- + +## @api.constrains + +For validation. Called on create and write. + +### Basic Usage + +```python +@api.constrains('email') +def _check_email(self): + for record in self: + if not tools.email_validation(record.email): + raise ValidationError("Invalid email") +``` + +### Multiple Fields + +```python +@api.constrains('date_start', 'date_end') +def _check_dates(self): + for record in self: + if record.date_end < record.date_start: + raise ValidationError("End date must be after start date") +``` + +### No Dotted Paths + +Unlike `@api.depends`, **cannot use dotted paths**: + +```python +# BAD: dotted path not supported +@api.constrains('partner_id.email') +def _check_email(self): + pass + +# GOOD: use simple field name +@api.constrains('partner_id') +def _check_email(self): + for record in self: + if not tools.email_validation(record.partner_id.email): + raise ValidationError("Invalid email") +``` + +--- + +## @api.ondelete + +For delete validation (Odoo 18+). + +```python +@api.ondelete(at_uninstall=False) +def _unlink_if_not_draft(self): + if any(rec.state != 'draft' for rec in self): + raise UserError("Cannot delete non-draft records") +``` + +### Parameters + +| Parameter | Description | +| -------------- | ------------------------------------------------------ | +| `at_uninstall` | If `False`, allows deletion when module is uninstalled | + +### Why Use @api.ondelete? + +- **Better than overriding `unlink()`**: Doesn't break module uninstall +- **Clear intent**: Explicitly for delete validation +- **Automatic**: Called before deletion + +### Unlink Override (Anti-pattern) + +```python +# BAD: breaks module uninstall +def unlink(self): + if any(rec.state != 'draft' for rec in self): + raise UserError("Cannot delete non-draft records") + return super().unlink() +``` + +--- + +## @api.onchange + +For form UI updates when field values change. + +### Basic Usage + +```python +@api.onchange('partner_id') +def _onchange_partner_id(self): + if self.partner_id: + self.email = self.partner_id.email + self.phone = self.partner_id.phone +``` + +### Multiple Fields + +```python +@api.onchange('country_id', 'state_id') +def _onchange_location(self): + if self.country_id: + # Update zip format + pass +``` + +### No CRUD Operations + +**Important**: `onchange` methods should **not** perform CRUD operations. + +```python +# BAD: create in onchange +@api.onchange('field1') +def _onchange_field1(self): + self.env['another.model'].create({'name': 'test'}) + +# GOOD: only modify current record +@api.onchange('field1') +def _onchange_field1(self): + self.field2 = 'computed value' +``` + +### Return Warning + +```python +@api.onchange('amount') +def _onchange_amount(self): + if self.amount < 0: + return { + 'warning': { + 'title': "Warning", + 'message': "Amount cannot be negative", + } + } +``` + +--- + +## @api.model + +For model-level methods that don't depend on `self`. + +### Usage + +```python +@api.model +def get_default_values(self): + return { + 'field1': 'value1', + 'field2': 'value2', + } +``` + +### Can be called on any recordset + +```python +# Can call on any recordset (self may be empty) +record = self.env['my.model'].browse([1, 2, 3]) +defaults = record.get_default_values() +``` + +--- + +## @api.model_create_multi + +For handling batch create operations. + +```python +@api.model_create_multi +def create(self, vals_list): + # Add default values + for vals in vals_list: + vals.setdefault('field', 'default') + return super().create(vals_list) +``` + +### Why Use It? + +Odoo 17+ creates records in batches by default. This decorator ensures proper handling. + +--- + +## @api.autovacuum + +For methods to run by cron vacuuem. + +```python +@api.autovacuum +def _gc_entries(self): + # Clean old records + domain = [('create_date', '<', date.today() - timedelta(days=90)]) + self.search(domain).unlink() +``` + +--- + +## Decorator Decision Tree + +``` +Need to define field behavior? +├── Field computed from other fields → @api.depends +│ └── CAN use dotted paths +├── Validate data → @api.constrains +│ └── CANNOT use dotted paths +├── Prevent record deletion → @api.ondelete +└── Update form UI → @api.onchange + └── NO CRUD operations allowed + +Need to define method behavior? +├── Method-level, doesn't depend on self → @api.model +├── Mark method as non-RPC callable → @api.private +└── Normal record method → no decorator needed +``` + +--- + +## @api.private + +New in Odoo 19. Marks a method as **not callable via RPC** (external API). + +### Usage + +```python +from odoo import api, models + +class MyModel(models.Model): + _name = 'my.model' + + @api.private + def _internal_computation(self): + """This method cannot be called via XML-RPC/JSON-RPC.""" + return self._do_heavy_work() + + def public_action(self): + """This method CAN be called via RPC.""" + return self._internal_computation() +``` + +### When to Use + +- Methods that should only be called internally (not via API/button) +- Replaces the convention of prefixing with `_` for security-critical methods +- ORM override methods that you don't want exposed + +### @api.private vs Underscore Convention + +```python +# Convention: underscore prefix = private (but NOT enforced by ORM) +def _do_stuff(self): # Cannot be called from action buttons, but still convention-based + pass + +# Odoo 19: @api.private = explicitly enforced by framework +@api.private +def compute_sensitive_data(self): # Name doesn't need underscore + pass +``` + +--- + +--- + +## @api.returns + +**Purpose**: Specify the return model of a method for API compatibility. + +```python +from odoo import api, models + +class SaleOrder(models.Model): + _name = 'sale.order' + + @api.returns('res.partner') + def get_partner(self): + """Returns partner record(s)""" + return self.mapped('partner_id') + + @api.returns('self') + def copy(self, default=None): + """Returns new record(s) of same model""" + return super().copy(default) +``` + +**Common usage in Odoo base**: +```python +# Many methods use @api.returns +@api.returns('mail.message', lambda value: value.id) +def message_post(self, ...): + # Post a message, return the message + return message +``` + +--- + +## Common Patterns + +### Computed Field with Inverse + +```python +total = fields.Float(compute='_compute_total', inverse='_inverse_total', store=True) + +@api.depends('subtotal', 'tax') +def _compute_total(self): + for record in self: + record.total = record.subtotal + record.tax + +def _inverse_total(self): + for record in self: + record.subtotal = record.total - record.tax +``` + +### Validation with Constraints + +```python +@api.constrains('age') +def _check_age(self): + for record in self: + if record.age < 18: + raise ValidationError("Must be 18 or older") +``` + +### Delete Validation + +```python +@api.ondelete(at_uninstall=False) +def _unlink_if_not_cancelled(self): + if any(rec.state != 'cancel' for rec in self): + raise UserError("Only cancelled records can be deleted") +``` + +### Onchange for Defaults + +```python +@api.onchange('partner_id') +def _onchange_partner_id(self): + if self.partner_id: + self.lang = self.partner_id.lang + self.user_id = self.partner_id.user_id +``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/orm.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-development-guide.md b/.agents/skills/odoo-19/references/odoo-19-development-guide.md new file mode 100644 index 00000000..c655aa50 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-development-guide.md @@ -0,0 +1,514 @@ +# Odoo 19 Development Guide + +Guide for developing Odoo 19 modules: creating modules, manifest, structure, and common patterns. + +## Table of Contents +- [Module Structure](#module-structure) +- [Creating a Module](#creating-a-module) +- [Manifest File](#manifest-file) +- [Models](#models) +- [Views](#views) +- [Security](#security) +- [Data Files](#data-files) +- [Assets](#assets) +- [Wizards](#wizards) + +--- + +## Module Structure + +### Standard Structure + +``` +my_module/ +├── __init__.py +├── __manifest__.py +├── models/ +│ ├── __init__.py +│ └── my_model.py +├── views/ +│ └── my_model_views.xml +├── security/ +│ ├── ir.model.access.csv +│ └── my_module_security.xml +├── data/ +│ └── my_module_data.xml +├── demo/ +│ └── demo_data.xml +├── migrations/ +│ └── 19.0.1.0/ +│ └── post-migration.py +├── tests/ +│ ├── __init__.py +│ └── test_my_model.py +├── wizard/ +│ ├── __init__.py +│ └── my_wizard.py +├── controllers/ +│ ├── __init__.py +│ └── my_controller.py +├── static/ +│ ├── src/ +│ │ ├── js/ +│ │ ├── xml/ +│ │ └── scss/ +│ └── description/ +│ └── icon.png +└── report/ + └── my_report.xml +``` + +--- + +## Creating a Module + +### Step 1: Create Directory + +```bash +mkdir -p my_module/models +mkdir -p my_module/views +mkdir -p my_module/security +mkdir -p my_module/static/src/js +``` + +### Step 2: Create __init__.py + +```python +# __init__.py + +from . import models +from . import controllers +``` + +```python +# models/__init__.py + +from . import my_model +``` + +### Step 3: Create Model + +```python +# models/my_model.py + +from odoo import models, fields + +class MyModel(models.Model): + _name = 'my.model' + _description = 'My Model' + + name = fields.Char(string="Name", required=True) + description = fields.Text(string="Description") + active = fields.Boolean(string="Active", default=True) +``` + +### Step 4: Create Manifest + +```python +# __manifest__.py + +{ + 'name': 'My Module', + 'version': '1.0.0', + 'category': 'Tools', + 'summary': 'My awesome module', + 'description': """ + My Module + ========== + This module does something useful. + """, + 'author': 'Your Name', + 'website': 'https://github.com/yourname/my_module', + 'license': 'LGPL-3', + 'depends': ['base'], + 'data': [ + 'security/my_module_security.xml', + 'views/my_model_views.xml', + ], + 'demo': [ + 'demo/demo_data.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'my_module/static/src/js/my_script.js', + ], + }, + 'installable': True, + 'application': False, +} +``` + +--- + +## Manifest File + +### Required Fields + +```python +{ + 'name': 'My Module', # Required + 'version': '1.0', # Optional + 'depends': ['base'], # Optional but recommended +} +``` + +### Common Fields + +```python +{ + # Information + 'name': 'My Module', + 'version': '1.0.0', + 'category': 'Tools', + 'summary': 'Short description', + 'description': 'Long description', + 'author': 'Author Name', + 'website': 'https://example.com', + 'license': 'LGPL-3', + + # Dependencies + 'depends': ['base', 'web'], + 'data': ['views/views.xml'], + 'demo': ['demo/demo.xml'], + + # Assets + 'assets': { + 'web.assets_backend': [ + 'my_module/static/src/js/file.js', + ], + }, + + # Other + 'application': False, + 'installable': True, + 'auto_install': False, +} +``` + +--- + +## Models + +### Basic Model + +```python +from odoo import models, fields + +class MyModel(models.Model): + _name = 'my.model' + _description = 'My Model' + _order = 'name' + + name = fields.Char(string="Name", required=True) + code = fields.Char(string="Code") + description = fields.Text(string="Description") + active = fields.Boolean(string="Active", default=True) +``` + +### Model Inheritance (Extension) + +```python +class Partner(models.Model): + _inherit = 'res.partner' + + my_field = fields.Char(string="My Field") +``` + +### Model Inheritance (Prototype) + +```python +class NewModel(models.Model): + _name = 'new.model' + _inherit = 'base.model' + + # Inherits all fields and methods from base.model + my_field = fields.Char(string="My Field") +``` + +--- + +## Views + +### Create Views + +```xml + + + + + my.model.tree + my.model + + + + + + + + + + + + my.model.form + my.model + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + + my.model.search + my.model + + + + + + + + + + + + My Models + my.model + list,form + +

+ Create your first my.model! +

+
+
+ + + + +
+``` + +--- + +## Security + +### Access Rights (CSV) + +File: `security/ir.model.access.csv` + +```csv +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_my_model_user,my.model.user,model_my_model,base.group_user,1,1,1,0 +access_my_model_manager,my.model.manager,model_my_model,group_my_module_manager,1,1,1,1 +``` + +### Record Rules (XML) + +```xml + + + + + My Model: user can see own records + + [('create_uid', '=', user.id)] + + + + + + + + + + My Model: manager sees all + + [(1, '=', 1)] + + + + +``` + +--- + +## Data Files + +### Create Records + +```xml + + + Record 1 + R001 + + + + Record 2 + R002 + + +``` + +### noupdate Flag + +```xml + + + + + Demo + + + + + + Core + + +``` + +--- + +## Assets + +### JavaScript + +```python +'assets': { + 'web.assets_backend': [ + 'my_module/static/src/js/my_widget.js', + 'my_module/static/src/js/my_view.js', + ], + 'web.assets_frontend': [ + 'my_module/static/src/js/frontend.js', + ], +}, +``` + +### CSS/SCSS + +```python +'assets': { + 'web.assets_backend': [ + 'my_module/static/src/scss/my_style.scss', + ], + 'web.assets_frontend': [ + 'my_module/static/src/scss/frontend.scss', + ], +}, +``` + +--- + +## Wizards + +### TransientModel + +```python +from odoo import models, fields + +class MyWizard(models.TransientModel): + _name = 'my.wizard' + _description = 'My Wizard' + + date = fields.Date(string="Date", required=True, default=fields.Date.context_today) + note = fields.Text(string="Note") + + def action_confirm(self): + # Do something + return {'type': 'ir.actions.act_window_close'} +``` + +### Wizard View + +```xml + + my.wizard.form + my.wizard + +
+ + + + +
+
+
+
+
+``` + +### Action to Open Wizard + +```python +def action_open_wizard(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'My Wizard', + 'res_model': 'my.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_date': fields.Date.context_today(self), + } + } +``` + +--- + +## Common Patterns + +### State Field + +```python +state = fields.Selection([ + ('draft', 'Draft'), + 'confirmed', 'Confirmed'), + ('done', 'Done'), +], string='State', default='draft', tracking=True) +``` + +### Create Default from Context + +```python +def default_get(self, fields_list): + defaults = super().default_get(fields_list) + if 'field' in fields_list: + defaults['field'] = self.env.context.get('default_field', 'default') + return defaults +``` + +### Name Search + +```python +def name_search(self, name='', args=None, operator='ilike', limit=100): + args = args or [] + if name: + args = [('name', operator, name)] + args + return super().name_search(name, args, operator, limit) +``` + +--- + +## References + +- Based on Odoo 19 best practices diff --git a/.agents/skills/odoo-19/references/odoo-19-field-guide.md b/.agents/skills/odoo-19/references/odoo-19-field-guide.md new file mode 100644 index 00000000..577fde15 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-field-guide.md @@ -0,0 +1,509 @@ +# Odoo 19 Field Guide + +Guide for defining fields in Odoo 19: field types, parameters, computed fields, and relational fields. + +## Table of Contents +- [Field Types](#field-types) +- [Basic Fields](#basic-fields) +- [Advanced Fields](#advanced-fields) +- [Date Fields](#date-fields) +- [Relational Fields](#relational-fields) +- [Computed Fields](#computed-fields) +- [Related Fields](#related-fields) +- [Field Parameters](#field-parameters) + +--- + +## Field Types + +### Type Summary + +| Category | Types | +|----------|-------| +| **Basic** | Boolean, Char, Float, Integer | +| **Advanced** | Binary, Html, Image, Monetary, Selection, Text | +| **Date** | Date, Datetime | +| **Relational** | Many2one, One2many, Many2many | +| **Pseudo** | Reference, Many2oneReference | + +--- + +## Basic Fields + +### Boolean + +True/False value. + +```python +active = fields.Boolean(string="Active", default=True) +is_company = fields.Boolean("Is Company") +``` + +### Char + +String with limited length. + +```python +name = fields.Char(string="Name", required=True) +code = fields.Char("Code", size=10) +``` + +### Float + +Floating-point number. + +```python +price = fields.Float(string="Price") +weight = fields.Float(digits="Stock Weight") +``` + +#### Digits + +```python +# Using named precision +price = fields.Float(digits="Product Price") + +# Custom precision (12 digits, 2 decimal) +amount = fields.Float(digits=(12, 2)) +``` + +### Integer + +Whole number. + +```python +count = fields.Integer(string="Count") +priority = fields.Integer(default=10) +``` + +--- + +## Advanced Fields + +### Binary + +Binary data (files). + +```python +file = fields.Binary(string="File") +attachment = fields.Binary("Attachment") +``` + +### Html + +HTML content (rich text). + +```python +description = fields.Html(string="Description") +notes = fields.Html("Notes", sanitize=False) +``` + +### Image + +Enhanced Binary for images with thumbnails. + +```python +image = fields.Image(string="Image") +logo = fields.Image("Logo", max_width=1024, max_height=1024) +``` + +### Monetary + +Monetary amount with currency. + +```python +amount = fields.Monetary(string="Amount", currency_field="currency_id") +``` + +**Requires** a currency field (default: `currency_id`). + +### Selection + +Selection from predefined list. + +```python +state = fields.Selection([ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('done', 'Done'), +], string="State", default='draft') + +# Or using model reference +type = fields.Selection([ + ('a', 'A'), + ('b', 'B'), +], string="Type") +``` + +**Dynamic selection** (from another model): + +```python +type_id = fields.Many2one('my.type', string="Type") +``` + +### Text + +Long text (unlimited). + +```python +description = fields.Text(string="Description") +notes = fields.Text("Notes") +``` + +--- + +## Date Fields + +### Date + +Date without time. + +```python +date = fields.Date(string="Date") +deadline = fields.Date(default=fields.Date.context_today) +``` + +#### Date Methods + +```python +from odoo.fields import Date + +# Today +today = Date.context_today(self) + +# Add/subtract +next_week = Date.add(Date.today(), weeks=1) +last_month = Date.subtract(Date.today(), months=1) + +# Start/end of period +start_of_month = Date.start_of(Date.today(), 'month') +end_of_month = Date.end_of(Date.today(), 'month') + +# To string +date_str = Date.to_string(Date.today()) + +# From string +date_obj = Date.to_date('2023-01-01') +``` + +### Datetime + +Date and time. + +```python +datetime = fields.Datetime(string="DateTime") +create_date = fields.Datetime(default=fields.Datetime.now) +``` + +#### Datetime Methods + +```python +from odoo.fields import Datetime + +# Now +now = Datetime.now() + +# Context timestamp (user timezone) +timestamp = Datetime.context_timestamp(self, datetime) + +# Add/subtract +next_hour = Datetime.add(Datetime.now(), hours=1) + +# Start/end of period +start_of_day = Datetime.start_of(Datetime.now(), 'day') +end_of_day = Datetime.end_of(Datetime.now(), 'day') + +# Convert +date_obj = Datetime.to_datetime('2023-01-01 12:00:00') +date_str = Datetime.to_string(Datetime.now()) +``` + +#### Timezone + +Datetime fields are stored as UTC. Conversion is client-side. + +--- + +## Relational Fields + +### Many2one + +Many-to-one relation (foreign key). + +```python +partner_id = fields.Many2one('res.partner', string="Partner") +user_id = fields.Many2one('res.users', 'User', default=lambda self: self.env.user) +``` + +#### Parameters + +| Parameter | Description | +|-----------|-------------| +| `comodel_name` | Related model name | +| `string` | Field label | +| `required` | Whether required | +| `ondelete` | What to do when related record is deleted (`cascade`, `set null`, `restrict`) | +| `domain` | Domain filter | +| `context` | Context for operations | +| `default` | Default value | +| `index` | Add database index | + +```python +partner_id = fields.Many2one( + 'res.partner', + string="Customer", + required=True, + ondelete='cascade', + domain=[('customer_rank', '>', 0)], + default=lambda self: self.env.partner, +) +``` + +### One2many + +One-to-many relation (inverse of Many2one). + +```python +line_ids = fields.One2many('sale.order.line', 'order_id', string="Order Lines") +``` + +#### Parameters + +| Parameter | Description | +|-----------|-------------| +| `comodel_name` | Related model name | +| `inverse_name` | Inverse Many2one field | +| `string` | Field label | + +```python +order_id = fields.Many2one('sale.order', 'Order') +line_ids = fields.One2many( + 'sale.order.line', + 'order_id', + string="Order Lines", +) +``` + +### Many2many + +Many-to-many relation. + +```python +tag_ids = fields.Many2many('crm.tag', string="Tags") +category_ids = fields.Many2many('product.category', 'product_category_rel', 'product_id', 'category_id') +``` + +#### Parameters + +| Parameter | Description | +|-----------|-------------| +| `comodel_name` | Related model name | +| `relation` | Relation table name (auto if not specified) | +| `column1` | Column for this model | +| `column2` | Column for related model | +| `string` | Field label | + +```python +# Simple (auto relation table) +tag_ids = fields.Many2many('crm.tag', string="Tags") + +# Custom relation table +product_ids = fields.Many2many( + 'product.product', + 'my_rel', + 'my_id', + 'product_id', + string="Products", +) +``` + +### Commands + +Use `Command` class for One2many/Many2many operations: + +```python +from odoo.fields import Command + +# Create +{ + 'line_ids': [ + Command.create({'product_id': 1, 'qty': 10}), + Command.create({'product_id': 2, 'qty': 5}), + ] +} + +# Update +{ + 'line_ids': [ + Command.update(line_id, {'qty': 20}), + ] +} + +# Delete +{ + 'line_ids': [ + Command.delete(line_id), + ] +} + +# Clear all +{ + 'line_ids': [Command.clear()] +} + +# Set (replace all) +{ + 'line_ids': [ + Command.set([id1, id2, id3]) + ] +} + +# Link (add without deleting existing) +{ + 'tag_ids': [ + Command.link(tag_id), + ] +} + +# Unlink +{ + 'tag_ids': [ + Command.unlink(tag_id), + ] +} +``` + +--- + +## Computed Fields + +Fields computed from other fields. + +### Basic Compute + +```python +total = fields.Float(compute='_compute_total') + +@api.depends('price', 'qty') +def _compute_total(self): + for record in self: + record.total = record.price * record.qty +``` + +### Store and Search + +```python +total = fields.Float( + compute='_compute_total', + store=True, + search='_search_total', +) +``` + +### Inverse + +Allow setting computed field: + +```python +full_name = fields.Char( + compute='_compute_full_name', + inverse='_inverse_full_name', +) + +@api.depends('first_name', 'last_name') +def _compute_full_name(self): + for record in self: + record.full_name = f"{record.first_name} {record.last_name}" + +def _inverse_full_name(self): + for record in self: + parts = record.full_name.split(' ', 1) + record.first_name = parts[0] + record.last_name = parts[1] if len(parts) > 1 else '' +``` + +--- + +## Related Fields + +Shortcut for computed fields that follow a relation. + +```python +partner_name = fields.Char(related='partner_id.name', string="Partner Name") +partner_email = fields.Char(related='partner_id.email', readonly=True) +``` + +### Store Related + +```python +partner_name = fields.Char( + related='partner_id.name', + string="Partner Name", + store=True, +) +``` + +### Dependencies + +```python +# Only recompute when partner_id changes +partner_name = fields.Char( + related='partner_id.name', + store=True, + depends=['partner_id'], +) +``` + +--- + +## Field Parameters + +### Common Parameters + +| Parameter | Description | +|-----------|-------------| +| `string` | Field label | +| `required` | Whether required (create/write) | +| `readonly` | Whether read-only | +| `index` | Add database index | +| `default` | Default value or callable | +| `help` | Tooltip text | +| `groups` | Comma-separated group IDs | +| `copy` | Copy on duplicate (default: True) | +| `track_visibilty` | Track changes in chatter (`always`, `onchange`, `never`) | + +### Examples + +```python +name = fields.Char( + string="Name", + required=True, + index=True, + default='Untitled', + help="Enter the name", + copy=True, + tracking=True, # Equivalent to track_visibility='always' +) +``` + +--- + +## Reserved Field Names + +| Name | Purpose | +|------|---------| +| `id` | Record identifier | +| `display_name` | Display name | +| `create_date`, `create_uid`, `write_date`, `write_uid` | Access log fields | +| `name` | Default `rec_name` | +| `active` | Global visibility toggle | +| `state` | Lifecycle stages | +| `parent_id` | Tree structure parent | +| `parent_path` | Tree structure path | +| `company_id` | Multi-company field | + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/orm.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-manifest-guide.md b/.agents/skills/odoo-19/references/odoo-19-manifest-guide.md new file mode 100644 index 00000000..04ab9103 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-manifest-guide.md @@ -0,0 +1,331 @@ +# Odoo 19 Manifest Guide + +Guide for configuring `__manifest__.py` in Odoo 19 modules. + +## Table of Contents +- [Manifest File](#manifest-file) +- [Required Fields](#required-fields) +- [Module Information](#module-information) +- [Dependencies](#dependencies) +- [Data Files](#data-files) +- [Assets](#assets) +- [Hooks](#hooks) +- [External Dependencies](#external-dependencies) +- [Auto-Install](#auto-install) + +--- + +## Manifest File + +The manifest file declares a python package as an Odoo module and specifies module metadata. + +File: `__manifest__.py` + +```python +{ + 'name': "A Module", + 'version': '1.0', + 'depends': ['base'], + 'author': "Author Name", + 'category': 'Category', + 'description': """ + Description text + """, + 'data': [ + 'views/mymodule_view.xml', + ], + 'demo': [ + 'demo/demo_data.xml', + ], +} +``` + +--- + +## Required Fields + +### name (str, required) + +The human-readable name of the module. + +```python +'name': "My Module", +``` + +--- + +## Module Information + +### version (str) + +Module version, should follow semantic versioning rules. + +```python +'version': '1.0.0', +``` + +### description (str) + +Extended description in reStructuredText. + +```python +'description': """ +This module does something useful. +""", +``` + +### author (str) + +Name of the module author. + +```python +'author': "UncleCat", +``` + +### website (str) + +Website URL for the module author. + +```python +'website': "https://github.com/unclecat", +``` + +### license (str, default: LGPL-3) + +Distribution license. + +Possible values: +- `GPL-2` +- `GPL-2 or any later version` +- `GPL-3` +- `GPL-3 or any later version` +- `AGPL-3` +- `LGPL-3` +- `Other OSI approved licence` +- `OEEL-1` (Odoo Enterprise Edition License v1.0) +- `OPL-1` (Odoo Proprietary License v1.0) +- `Other proprietary` + +```python +'license': 'LGPL-3', +``` + +### category (str, default: Uncategorized) + +Classification category within Odoo. + +Use existing categories or create hierarchies with `/`: + +```python +'category': 'Tools / My Category', +``` + +### application (bool, default: False) + +Whether the module should be considered a fully-fledged application. + +```python +'application': True, # Appears in Apps menu +``` + +### installable (bool, default: True) + +Whether a user can install the module from the Web UI. + +```python +'installable': False, # Hidden from Apps menu +``` + +### maintainer (str) + +Person or entity in charge of maintenance. + +```python +'maintainer': "UncleCat", +``` + +--- + +## Dependencies + +### depends (list(str)) + +Odoo modules which must be loaded before this one. + +```python +'depends': ['base', 'web', 'sale'], +``` + +**Important**: Module `base` is always installed, but you should still specify it as a dependency to ensure your module is updated when `base` is updated. + +When a module is installed, all dependencies are installed first. + +--- + +## Data Files + +### data (list(str)) + +Data files always loaded at installation and update. + +```python +'data': [ + 'security/my_module_security.xml', + 'views/my_model_views.xml', + 'data/my_module_data.xml', +], +``` + +### demo (list(str)) + +Data files only loaded in demonstration mode. + +```python +'demo': [ + 'demo/demo_data.xml', +], +``` + +--- + +## Assets + +### assets (dict) + +Definition of how static files are loaded in asset bundles. + +```python +'assets': { + 'web.assets_backend': [ + 'my_module/static/src/js/my_script.js', + 'my_module/static/src/scss/my_style.scss', + ], + 'web.assets_frontend': [ + 'my_module/static/src/js/frontend.js', + ], +}, +``` + +--- + +## Hooks + +### {pre_init, post_init, uninstall}_hook (str) + +Hooks for module installation/uninstallation. + +```python +# In __init__.py +def pre_init_hook(env): + """Executed prior to module installation""" + pass + +def post_init_hook(env): + """Executed right after module installation""" + pass + +def uninstall_hook(env): + """Executed after module uninstallation""" + pass +``` + +```python +# In __manifest__.py +'pre_init_hook': 'pre_init_hook', +'post_init_hook': 'post_init_hook', +'uninstall_hook': 'uninstall_hook', +``` + +**Usage**: Only when setup/cleanup is extremely difficult or impossible through the API. + +--- + +## External Dependencies + +### external_dependencies (dict(key=list(str))) + +Dictionary of Python and binary dependencies. + +```python +'external_dependencies': { + 'python': ['requests', 'openpyxl'], + 'bin': ['zip', 'unzip'], +}, +``` + +The module won't be installed if dependencies are not available. + +--- + +## Auto-Install + +### auto_install (bool or list(str), default: False) + +If `True`, automatically installs if all dependencies are installed. + +Used for "link modules" implementing integration between independent modules. + +```python +'auto_install': True, # Install when all dependencies are present +``` + +If it is a list, must contain a subset of dependencies: + +```python +'auto_install': ['sale', 'crm'], # Install when both sale and crm are present +``` + +If the list is empty, always auto-install regardless of dependencies. + +```python +'auto_install': [], # Always install +``` + +--- + +## Complete Example + +```python +{ + 'name': "My Awesome Module", + 'version': '1.0.0', + 'category': 'Tools', + 'summary': 'Does something awesome', + 'description': """ +My Awesome Module +================= + +This module adds awesome functionality to Odoo. +""", + 'author': "UncleCat", + 'website': "https://github.com/unclecat/my_module", + 'license': 'LGPL-3', + 'depends': ['base', 'web'], + 'data': [ + 'security/my_module_security.xml', + 'views/my_model_views.xml', + 'data/ir_cron_data.xml', + ], + 'demo': [ + 'demo/demo_data.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'my_module/static/src/js/my_widget.js', + 'my_module/static/src/scss/my_style.scss', + ], + }, + 'external_dependencies': { + 'python': ['requests'], + }, + 'application': True, + 'installable': True, + 'auto_install': False, +} +``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/module.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-migration-guide.md b/.agents/skills/odoo-19/references/odoo-19-migration-guide.md new file mode 100644 index 00000000..2927ab0d --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-migration-guide.md @@ -0,0 +1,244 @@ +# Odoo 19 Migration Guide + +Guide for migrating modules from Odoo 17/18 to Odoo 19. + +## Table of Contents +- [Migration Overview](#migration-overview) +- [Key Changes](#key-changes) +- [Migration Scripts](#migration-scripts) +- [Module Hooks](#module-hooks) +- [Common Migrations](#common-migrations) +- [Testing](#testing) + +--- + +## Migration Overview + +### When to Migrate + +- **Major version upgrade**: Odoo 17 → 18 → 19 +- **Module dependencies changed** +- **API breaking changes** + +### Migration Strategy + +1. Review changelog and breaking changes +2. Update `__manifest__.py` version +3. Run migration scripts +4. Test thoroughly +5. Update documentation + +--- + +## Key Changes + +### Odoo 19 Key Changes + +| Area | Change | +|------|--------| +| **List view** | Use `` instead of `` | +| **Dynamic attributes** | Use direct attributes instead of `attrs` | +| **Delete validation** | Use `@api.ondelete` instead of overriding `unlink()` | +| **Field aggregation** | Use `aggregator=` instead of `group_operator=` | +| **SQL queries** | Use `odoo.tools.SQL` class | +| **Batch create** | Use list of dicts instead of single dict | + +### Odoo 18 Key Changes + +| Area | Change | +|------|--------| +| **Views** | Use `` instead of `` | +| **attrs** | Deprecated, use direct attributes | +| **ondelete** | New `@api.ondelete` decorator | + +--- + +## Migration Scripts + +### Migration Script Location + +``` +my_module/ +└── migrations/ + └── 19.0.1.0/ + ├── pre-migration.py + ├── end-migration.py + └── post-migration.py +``` + +### Migration Script Naming + +| Script | When it runs | +|--------|--------------| +| `pre-migration.py` | Before module update | +| `post-migration.py` | After module update | +| `end-migration.py` | After all migrations | + +### Migration Script Template + +```python +def migrate(cr, version): + """ + Migration script for Odoo 19 + """ + # Your migration code here + pass +``` + +--- + +## Common Migrations + +### Tree to List View + +**Odoo 17 and earlier**: + +```xml + + + +``` + +**Odoo 18+**: + +```xml + + + +``` + +### attrs to Direct Attributes + +**Odoo 17 and earlier**: + +```xml + +``` + +**Odoo 18+**: + +```xml + +``` + +### Delete Validation + +**Odoo 17 and earlier**: + +```python +def unlink(self): + for record in self: + if record.state != 'draft': + raise UserError("Cannot delete non-draft records") + return super().unlink() +``` + +**Odoo 18+**: + +```python +@api.ondelete(at_uninstall=False) +def _unlink_if_not_draft(self): + if any(rec.state != 'draft' for rec in self): + raise UserError("Cannot delete non-draft records") +``` + +### Field Aggregation + +**Odoo 17 and earlier**: + +```python +amount_total = fields.Monetary(group_operator="sum") +``` + +**Odoo 18+**: + +```python +amount_total = fields.Monetary(aggregator="sum") +``` + +--- + +## Module Hooks + +### pre_init_hook + +```python +def pre_init_hook(env): + """Called before module installation""" + # Create custom tables, etc. + pass +``` + +### post_init_hook + +```python +def post_init_hook(env): + """Called after module installation""" + # Set default values, create records, etc. + pass +``` + +### uninstall_hook + +```python +def uninstall_hook(env): + """Called after module uninstallation""" + # Clean up custom tables, files, etc. + pass +``` + +### Register Hooks in Manifest + +```python +{ + ... + 'pre_init_hook': 'my_module.pre_init_hook', + 'post_init_hook': 'my_module.post_init_hook', + 'uninstall_hook': 'my_module.uninstall_hook', +} +``` + +--- + +## Migration Checklist + +- [ ] Review Odoo 19 changelog +- [ ] Update `__manifest__.py` version +- [ ] Update dependencies +- [ ] Rename `` to `` +- [ ] Replace `attrs` with direct attributes +- [ ] Replace `unlink()` override with `@api.ondelete` +- [ ] Update field aggregators +- [ ] Update SQL queries to use `odoo.tools.SQL` +- [ ] Run migration scripts +- [ ] Test all functionality +- [ ] Update documentation + +--- + +## Testing Migrations + +### Test Migration Script + +```python +from odoo.tests import TransactionCase + +class TestMigration(TransactionCase): + def test_migration(self): + # Test migration script + pass +``` + +### Manual Testing + +1. Install previous version with sample data +2. Update to Odoo 19 +3. Verify all data migrated correctly +4. Test all features + +--- + +## References + +- Odoo 19 changelog +- Odoo 18 changelog diff --git a/.agents/skills/odoo-19/references/odoo-19-mixins-guide.md b/.agents/skills/odoo-19/references/odoo-19-mixins-guide.md new file mode 100644 index 00000000..56ea7d4a --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-mixins-guide.md @@ -0,0 +1,384 @@ +# Odoo 19 Mixins Guide + +Guide for using Odoo 19 mixins: mail.thread, activities, email aliases, and other useful mixins. + +## Table of Contents +- [Messaging Features](#messaging-features) +- [Activities](#activities) +- [Email Aliases](#email-aliases) +- [UTM Mixin](#utm-mixin) +- [Website Mixins](#website-mixins) +- [Rating Mixin](#rating-mixin) + +--- + +## Messaging Features + +### Basic Messaging Integration + +Add `mail.thread` mixin to your model: + +```python +class BusinessTrip(models.Model): + _name = 'business.trip' + _inherit = ['mail.thread'] + _description = 'Business Trip' + + name = fields.Char() + partner_id = fields.Many2one('res.partner', 'Responsible') + guest_ids = fields.Many2many('res.partner', 'Participants') +``` + +Add chatter to form view: + +```xml +
+ + + +``` + +### Chatter Options + +| Option | Description | +|--------|-------------| +| `open_attachments` | Shows attachment section expanded | +| `reload_on_attachment` | Reload form on attachment change | +| `reload_on_follower` | Reload form on follower update | +| `reload_on_post` | Reload form on message post | + +--- + +### Posting Messages + +#### message_post + +Post a new message in an existing thread: + +```python +record.message_post( + body='This is a message', + subject='Subject', + message_type='notification', + subtype_xmlid='mail.mt_comment', +) +``` + +**Parameters**: +- `body` (str | Markup): Message body (escaped if str, use Markup for HTML) +- `subject` (str): Message subject +- `message_type` (str): `notification`, `comment`, `email` +- `subtype` (str/xmlid): Message subtype +- `parent_id` (int): Reply to message +- `attachments` (list): List of `(name, content)` tuples +- `**kwargs`: Extra mail.message field values + +#### message_post_with_view + +Send message using a QWeb template: + +```python +record.message_post_with_view( + 'my_module.my_template', + additional_context={'val': value}, +) +``` + +#### message_post_with_template + +Send message using an email template: + +```python +record.message_post_with_template( + template_id, + composition_mode='comment', +) +``` + +--- + +### Receiving Messages + +#### message_new + +Called when new email arrives for an alias: + +```python +def message_new(self, msg_dict, custom_values=None): + # Extract data from email + name = msg_dict.get('subject', 'New') + # Create record + return super().message_new(msg_dict, { + 'name': name, + **(custom_values or {}), + }) +``` + +#### message_update + +Called when email reply arrives: + +```python +def message_update(self, msg_dict, update_vals=None): + # Update record from email + return super().message_update(msg_dict, { + 'description': msg_dict.get('body'), + **(update_vals or {}), + }) +``` + +--- + +### Followers Management + +#### message_subscribe + +Add partners/channels as followers: + +```python +# Subscribe partners +record.message_subscribe(partner_ids=[pid1, pid2]) + +# Subscribe channels +record.message_subscribe(channel_ids=[cid1]) + +# With specific subtypes +record.message_subscribe( + partner_ids=[pid1], + subtype_ids=[subtype_id], +) +``` + +#### message_unsubscribe + +Remove followers: + +```python +# Unsubscribe partners +record.message_unsubscribe(partner_ids=[pid1, pid2]) + +# Unsubscribe current user +record.message_unsubscribe_users() +``` + +--- + +### Logging Changes (Tracking) + +Enable field tracking in `mail.thread`: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['mail.thread'] + + name = fields.Char(tracking=True) + state = fields.Selection([ + ('draft', 'Draft'), + ('done', 'Done'), + ], tracking=True) + + # Track changes in relational field + partner_id = fields.Many2one('res.partner', tracking=1) +``` + +Track changes in specific subfields: + +```python +# Track all partner_id subfields +partner_id = fields.Many2one('res.partner', tracking=True) + +# Track only name +partner_id = fields.Many2one('res.partner', tracking='name') +``` + +--- + +## Activities + +### mail.activity.mixin + +Add activity support: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['mail.activity.mixin'] + + name = fields.Char() +``` + +### Activity Methods + +```python +# Schedule activity +record.activity_schedule( + 'mail.mail_activity_data_todo', + user_id=user.id, + summary='Review this', +) + +# Mark as done +activities = record.activity_ids +activities.action_done() + +# Feedback +activities.action_feedback(feedback='Completed') +``` + +--- + +## Email Aliases + +### mail.alias.mixin + +Add email alias support: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['mail.alias.mixin', 'mail.thread'] + + name = fields.Char() + alias_id = fields.Many2one( + 'mail.alias', + string='Alias', + ondelete="cascade", + required=True, + ) + + def get_alias_model_name(self, vals): + return self._name + + def get_alias_values(self): + values = super().get_alias_values() + values.update({ + 'alias_defaults': 'name', + }) + return values +``` + +Create alias in data file: + +```xml + + my-model + + + +``` + +--- + +## UTM Mixin + +### utm.mixin + +Add campaign tracking: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['utm.mixin'] + + name = fields.Char() + campaign_id = fields.Many2one('utm.campaign', 'Campaign') + source_id = fields.Many2one('utm.source', 'Source') + medium_id = fields.Many2one('utm.medium', 'Medium') +``` + +This adds tracking for marketing campaigns. + +--- + +## Website Mixins + +### website.published.mixin + +Add website publishing: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['website.published.mixin'] + + name = fields.Char() + website_published = fields.Boolean('Visible on Website') +``` + +### website.seo.metadata + +Add SEO metadata: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['website.seo.metadata'] + + name = fields.Char() + website_meta_title = fields.Char('Meta Title') + website_meta_description = fields.Text('Meta Description') +``` + +--- + +## Rating Mixin + +### rating.mixin + +Add customer rating: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['rating.mixin', 'mail.thread'] + + name = fields.Char() +``` + +### Rating Methods + +```python +# Send rating request +record.rating_send_request( + rating_template='mail.mail_template_data_rating', +) + +# Get rating stats +avg_rating = record.rating_get_stats() +``` + +--- + +## Portal Access + +### portal.mixin + +Add customer portal access: + +```python +class MyModel(models.Model): + _name = 'my.model' + _inherit = ['portal.mixin'] + + name = fields.Char() + partner_id = fields.Many2one('res.partner', 'Customer') +``` + +Override access: + +```python +def _compute_access_url(self): + super()._compute_access_url() + for record in self: + record.access_url = '/my/model/%s' % record.id + +def _get_portal_return_action(self): + return self.env.ref('my_module.my_model_action') +``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/mixins.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-model-guide.md b/.agents/skills/odoo-19/references/odoo-19-model-guide.md new file mode 100644 index 00000000..2fbf5579 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-model-guide.md @@ -0,0 +1,642 @@ +# Odoo 19 Model Guide + +Guide for working with Odoo 19 ORM, recordsets, CRUD operations, and domain filters. + +## Table of Contents + +- [Models](#models) +- [Fields](#fields) +- [SQL Constraints](#sql-constraints) +- [Database Indexes](#database-indexes) +- [Recordsets](#recordsets) +- [CRUD Operations](#crud-operations) +- [Search Domains](#search-domains) +- [Environment](#environment) +- [SQL Execution](#sql-execution) +- [Inheritance](#inheritance) + +--- + +## Models + +### Defining a Model + +> **Odoo 19 Change**: `_name` is now **optional**. Odoo derives it automatically from the CamelCase class name (each capital letter → `.` separator). E.g. `ResPartner` → `res.partner`, `SaleOrder` → `sale.order`. + +```python +from odoo import models, fields + +# Odoo 19: _name auto-derived from class name +class MyModel(models.Model): + # _name = 'my.model' ← auto-derived, can be omitted + _description = 'My Model' + + field1 = fields.Char() + field2 = fields.Integer(string="Field Label") +``` + +```python +# When _name differs from class name convention, specify explicitly: +class CustomNameModel(models.Model): + _name = 'custom.different.name' + _description = 'Custom Named Model' + + name = fields.Char() +``` + +### Model Attributes + +| Attribute | Description | +| --------------- | ----------------------------------------------------------------------------- | +| `_name` | Model name (**optional in Odoo 19** — auto-derived from CamelCase class name) | +| `_description` | Model description | +| `_order` | Default sort order | +| `_rec_name` | Field to use as name representation | +| `_inherit` | Model(s) to inherit from | +| `_inherits` | Delegation inheritance | +| `_table` | Database table name | +| `_log_access` | Enable create_date, write_date, create_uid, write_uid | +| `_auto` | Auto-create database table | +| `_abstract` | Abstract model | +| `_transient` | Transient model | +| `_parent_store` | Enable parent_path field | +| `_fold_name` | Field for kanban fold | + +### Model Types + +| Class | Description | +| ----------------------- | ---------------------------------- | +| `models.Model` | Regular database model | +| `models.TransientModel` | Temporary/wizard model | +| `models.AbstractModel` | Abstract model (no database table) | + +--- + +## Fields + +### Field Definition + +Fields are defined as class attributes on the model. + +```python +from odoo import models, fields + +class MyModel(models.Model): + _name = 'my.model' + + name = fields.Char(required=True) + description = fields.Text() + active = fields.Boolean(default=True) + count = fields.Integer() + price = fields.Float(digits='Product Price') +``` + +### Default Values + +```python +# Value +name = fields.Char(default="A value") + +# Function +def _default_name(self): + return self.get_value() + +name = fields.Char(default=lambda self: self._default_name()) +``` + +### Field Types + +| Type | Class | Description | +| ----------------- | ---------------------------- | --------------------------- | +| **Basic** | | | +| Boolean | `fields.Boolean()` | True/False | +| Char | `fields.Char()` | String (limited length) | +| Float | `fields.Float()` | Floating-point number | +| Integer | `fields.Integer()` | Integer | +| **Advanced** | | | +| Binary | `fields.Binary()` | Binary data (files) | +| Html | `fields.Html()` | HTML content | +| Image | `fields.Image()` | Image (enhanced Binary) | +| Monetary | `fields.Monetary()` | Monetary amount | +| Selection | `fields.Selection()` | Selection from list | +| Text | `fields.Text()` | Long text | +| **Date** | | | +| Date | `fields.Date()` | Date (no time) | +| Datetime | `fields.Datetime()` | Date and time | +| **Relational** | | | +| Many2one | `fields.Many2one()` | Many-to-one | +| One2many | `fields.One2many()` | One-to-many | +| Many2many | `fields.Many2many()` | Many-to-many | +| **Pseudo** | | | +| Reference | `fields.Reference()` | Reference to any model | +| Many2oneReference | `fields.Many2oneReference()` | Many2one with dynamic model | + +### Computed Fields + +```python +from odoo import api + +total = fields.Float(compute='_compute_total', store=True) + +@api.depends('value', 'tax') +def _compute_total(self): + for record in self: + record.total = record.value + record.value * record.tax +``` + +### Related Fields + +```python +nickname = fields.Char(related='partner_id.name', store=True) +``` + +### Automatic Fields + +| Field | Type | Description | +| -------------- | -------- | --------------------- | +| `id` | int | Identifier | +| `display_name` | char | Display name | +| `create_date` | datetime | Creation timestamp | +| `create_uid` | Many2one | Creator | +| `write_date` | datetime | Last update timestamp | +| `write_uid` | Many2one | Last modifier | + +### Reserved Field Names + +| Name | Type | Purpose | +| ------------- | --------- | ------------------------- | +| `name` | Char | Default `rec_name` | +| `active` | Boolean | Toggles global visibility | +| `state` | Selection | Lifecycle stages | +| `parent_id` | Many2one | Tree structure parent | +| `parent_path` | Char | Tree structure path | +| `company_id` | Many2one | Multi-company field | + +--- + +## SQL Constraints + +> **Odoo 19 Breaking Change**: `_sql_constraints` is **no longer supported**. Use `models.Constraint` instead. + +### models.Constraint (Odoo 19) + +SQL constraints are now defined as model attributes using `models.Constraint`: + +```python +from odoo import models, fields + +class MyModel(models.Model): + _name = 'my.model' + _description = 'My Model' + + name = fields.Char(required=True) + code = fields.Char() + quantity = fields.Integer() + + # UNIQUE constraint + _unique_name = models.Constraint( + 'UNIQUE(name)', + 'Name must be unique!', + ) + + # UNIQUE on multiple fields + _unique_name_code = models.Constraint( + 'UNIQUE(name, code)', + 'The combination of name and code must be unique!', + ) + + # CHECK constraint + _check_quantity = models.Constraint( + 'CHECK(quantity > 0)', + 'Quantity must be positive!', + ) +``` + +### Constraint Naming + +If you omit the `_name` in the constraint, Odoo auto-generates a unique name based on model + attribute name. + +### Migration from `_sql_constraints` + +```python +# ❌ OLD (Odoo 18 and earlier) — NO LONGER WORKS in Odoo 19 +class MyModel(models.Model): + _name = 'my.model' + _sql_constraints = [ + ('name_uniq', 'UNIQUE(name)', 'Name must be unique!'), + ('check_qty', 'CHECK(quantity > 0)', 'Quantity must be positive!'), + ] + +# ✅ NEW (Odoo 19) +class MyModel(models.Model): + _name = 'my.model' + + _name_uniq = models.Constraint( + 'UNIQUE(name)', + 'Name must be unique!', + ) + + _check_qty = models.Constraint( + 'CHECK(quantity > 0)', + 'Quantity must be positive!', + ) +``` + +### Overriding Constraints in Inherited Models + +Constraints can be overridden or removed in inherited models: + +```python +class ExtendedModel(models.Model): + _inherit = 'my.model' + + # Override constraint with different check + _check_qty = models.Constraint( + 'CHECK(quantity >= 0)', + 'Quantity cannot be negative!', + ) +``` + +--- + +## Database Indexes + +### Field-Level Index + +```python +name = fields.Char(index=True) # Simple btree index +``` + +### Declarative Index (Odoo 19) + +For composite or custom indexes, use `models.Index`: + +```python +class MyModel(models.Model): + _name = 'my.model' + + name = fields.Char() + code = fields.Char() + date = fields.Date() + + # Composite index on multiple fields + _name_code_idx = models.Index('(name, code)') + + # Index with specific method + _date_idx = models.Index('(date DESC)') +``` + +> **Warning**: Don't over-index — indexes consume space and impact INSERT/UPDATE/DELETE performance. + +--- + +## Recordsets + +### Active Record Interface + +```python +# Read field +record.name +record.company_id.name + +# Write field +record.name = "Bob" + +# Dynamic field access +field = "name" +record[field] +``` + +### Iteration + +```python +def do_operation(self): + for record in self: + # record is a single record + print(record.name) +``` + +### Record Cache and Prefetching + +Odoo maintains a cache and prefetches records/fields following heuristics. + +```python +# Without prefetching: 2000 queries +for partner in partners: + print(partner.name) + print(partner.lang) + +# With prefetching: 1 query +for partner in partners: + print(partner.name) + print(partner.lang) +``` + +--- + +## CRUD Operations + +### Create + +```python +# Single record +record = self.env['model.name'].create({'field': 'value'}) + +# Multiple records (batch) +records = self.env['model.name'].create([ + {'field': 'value1'}, + {'field': 'value2'}, +]) +``` + +### Read + +```python +# Browse +record = self.env['model.name'].browse(record_id) +records = self.env['model.name'].browse([id1, id2, id3]) + +# Read +data = records.read(['field1', 'field2']) +``` + +### Write + +```python +# Single record +record.write({'field': 'value'}) + +# Multiple records +records.write({'field': 'value'}) +``` + +### Unlink (Delete) + +```python +# Single record +record.unlink() + +# Multiple records +records.unlink() +``` + +--- + +## Search Domains + +A search domain is a first-order logical predicate for filtering. + +### Domain Condition + +```python +# Simple condition +domain = [('name', '=', 'ABC')] + +# Multiple conditions +domain = [('name', '=', 'ABC'), ('phone', 'like', '7620')] +``` + +### Operators + +| Operator | Description | +| ------------------------------------ | ------------------ | +| `=` | equals | +| `!=` | not equals | +| `>`, `>=`, `<`, `<=` | comparison | +| `=?` | unset or equals | +| `=like`, `like`, `ilike`, `=ilike` | pattern matching | +| `in`, `not in` | in list | +| `child_of`, `parent_of` | tree traversal | +| `any`, `any!`, `not any`, `not any!` | relation traversal | + +### Logical Operators + +```python +# AND (implicit) +domain = [('name', '=', 'ABC'), ('state', '=', 'draft')] + +# OR +domain = '|', [('name', '=', 'ABC')], [('name', '=', 'XYZ')] + +# NOT +domain = '!', [('state', '=', 'draft')] +``` + +### Domain Class + +```python +from odoo.fields import Domain + +# Create domain +d1 = Domain('name', '=', 'abc') +d2 = Domain('phone', 'like', '7620') + +# Combine +d3 = d1 & d2 # AND +d4 = d1 | d2 # OR +d5 = ~d1 # NOT + +# Parse from list +domain = Domain([('name', '=', 'abc'), ('phone', 'like', '7620')]) + +# Serialize to list +domain_list = list(domain) +``` + +### Search Methods + +```python +# Search +records = self.env['model'].search(domain) + +# Search with limit +records = self.env['model'].search(domain, limit=10) + +# Search with offset +records = self.env['model'].search(domain, offset=20) + +# Search with order +records = self.env['model'].search(domain, order='name ASC') + +# Search count +count = self.env['model'].search_count(domain) + +# Search and read +records = self.env['model'].search_read(domain, ['field1', 'field2']) + +# Search fetch (Odoo 19+) +records = self.env['model'].search_fetch(domain, ['field1', 'field2']) + +# Name search +records = self.env['model'].name_search('keyword', operator='ilike') +``` + +--- + +## Environment + +The environment holds: + +- Database cursor (`cr`) +- Current user (`user`, `uid`) +- Context (`context`) +- Record cache + +### Accessing Environment + +```python +# From recordset +env = record.env + +# Create new recordset in another model +model = env['another.model'] + +# Access properties +env.uid # Current user id +env.user # Current user recordset +env.company # Current company +env.companies # Allowed companies +env.lang # Current language +``` + +### Altering Environment + +```python +# Change context +records.with_context(lang='fr_FR') + +# Change user +records.with_user(user_id) + +# Change company +records.with_company(company_id) + +# Change environment completely +records.with_env(new_env) + +# Sudo (superuser mode) +records.sudo() +``` + +--- + +## SQL Execution + +### Raw SQL + +```python +# Execute query +self.env.cr.execute("SELECT id FROM table WHERE field = %s", (value,)) + +# Fetch results +results = self.env.cr.fetchall() +row = self.env.cr.fetchone() +``` + +### SQL Class (Recommended) + +```python +from odoo.tools import SQL + +# Build query +query = SQL("SELECT id FROM table WHERE field = %s", value) + +# Execute +self.env.cr.execute(query) +``` + +### Flush and Invalidate + +Before SQL queries, flush pending data: + +```python +# Flush all records of a model +self.env['model'].flush_model(['field1', 'field2']) + +# Flush specific recordset +records.flush_recordset(['field1', 'field2']) +``` + +After SQL modifications, invalidate cache: + +```python +# Invalidate all records of a model +self.env['model'].invalidate_model(['field1', 'field2']) + +# Invalidate specific recordset +records.invalidate_recordset(['field1', 'field2']) + +# Notify field modification +records.modified(['field1', 'field2']) +``` + +--- + +## Inheritance + +### Classical Inheritance + +Create new model from existing one: + +```python +class Inheritance1(models.Model): + _name = 'inheritance.1' + _description = 'Inheritance One' + + name = fields.Char() + +class Inheritance2(models.Model): + _name = 'inheritance.2' + _inherit = ['inheritance.1'] + _description = 'Inheritance Two' + + # Inherits name field from inheritance.1 + # Adds new fields/methods +``` + +### Extension + +Extend existing model in-place: + +```python +class Extension0(models.Model): + _name = 'extension.0' + _description = 'Extension zero' + + name = fields.Char(default="A") + +class Extension0(models.Model): + _inherit = 'extension.0' + + description = fields.Char(default="Extended") +``` + +### Delegation + +Delegate fields to child records: + +```python +class Screen(models.Model): + _name = 'delegation.screen' + + size = fields.Float(string='Screen Size') + +class Laptop(models.Model): + _name = 'delegation.laptop' + + _inherits = { + 'delegation.screen': 'screen_id', + } + + name = fields.Char(string='Name') + screen_id = fields.Many2one('delegation.screen', required=True, ondelete="cascade") + +# Can access size directly on laptop +laptop.size +``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/orm.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-owl-guide.md b/.agents/skills/odoo-19/references/odoo-19-owl-guide.md new file mode 100644 index 00000000..0f089f1a --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-owl-guide.md @@ -0,0 +1,421 @@ +# Odoo 19 OWL Guide + +Guide for building OWL (Owl Web Library) components in Odoo 19. + +## Table of Contents + +- [OWL Overview](#owl-overview) +- [Component Structure](#component-structure) +- [Hooks](#hooks) +- [Services](#services) +- [State Management](#state-management) +- [QWeb Templates](#qweb-templates) +- [Translations](#translations) + +--- + +## OWL Overview + +OWL is a JavaScript framework for building web UI components in Odoo. + +### Key Concepts + +| Concept | Description | +| ------------- | -------------------------- | +| **Component** | Reusable UI building block | +| **State** | Reactive data | +| **Props** | Component properties | +| **Hooks** | Lifecycle functions | +| **Template** | QWeb template | + +--- + +## Component Structure + +### Basic Component + +```javascript +import { Component } from "@odoo/owl"; + +export class MyComponent extends Component { + static template = "my_module.MyComponent"; + static props = { + value: { type: String, optional: true }, + }; + + setup() { + // Component setup + } +} +``` + +### Register Component + +```javascript +import { registry } from "@web/core/registry"; + +registry.category("actions").add("my_component", MyComponent); +``` + +### Use in View + +```xml + +``` + +--- + +## Hooks + +### Setup Hook + +Called when component is created: + +```javascript +setup() { + // Initialize state + this.state = useState({ count: 0 }); + + // Call services + this.rpc = useService("rpc"); + this.orm = useService("orm"); + this.action = useService("action"); +} +``` + +### Lifecycle Hooks + +| Hook | When | +| ----------------- | --------------------- | +| `setup()` | Component creation | +| `onWillStart()` | Before render (async) | +| `onMounted()` | After render | +| `onWillUnmount()` | Before destroy | +| `onWillPatch()` | Before update | +| `onPatched()` | After update | + +### Example + +```javascript +setup() { + onWillStart(this.onWillStart); + onMounted(this.onMounted); + onWillUnmount(this.onWillUnmount); +} + +async onWillStart() { + // Load data before render +} + +onMounted() { + // After render +} + +onWillUnmount() { + // Cleanup +} +``` + +--- + +## Services + +### Common Services + +| Service | Description | +| -------------- | ------------------- | +| `orm` | Database operations | +| `rpc` | RPC calls | +| `action` | Execute actions | +| `dialog` | Show dialogs | +| `notification` | Show notifications | +| `router` | Navigation | +| `user` | Current user | +| `company` | Current company | + +### Use Service + +```javascript +setup() { + this.orm = useService("orm"); + this.rpc = useService("rpc"); + this.action = useService("action"); + this.dialog = useService("dialog"); + this.notification = useService("notification"); +} +``` + +### ORM Service + +```javascript +// Search +const records = await this.orm.search("my.model", [["active", "=", true]]); + +// Read +const data = await this.orm.read("my.model", ids, ["name", "value"]); + +// Create +const id = await this.orm.create("my.model", { name: "Test" }); + +// Write +await this.orm.write("my.model", [id], { name: "Updated" }); + +// Unlink +await this.orm.unlink("my.model", [id]); +``` + +### RPC Service + +```javascript +// Call controller +const result = await this.rpc("/my/controller", { param: "value" }); +``` + +### Action Service + +```javascript +// Do action +await this.action.doAction({ + type: "ir.actions.act_window", + res_model: "my.model", + views: [ + [false, "list"], + [false, "form"], + ], +}); +``` + +### Dialog Service + +```javascript +// Add dialog +this.dialog.add(MyDialog, { + title: "My Dialog", + confirm: () => {...}, +}); +``` + +### Notification Service + +```javascript +// Show notification +this.notification.notify({ + message: "Success!", + type: "success", +}); +``` + +--- + +## State Management + +### useState + +```javascript +setup() { + this.state = useState({ + count: 0, + name: "", + }); +} + +increment() { + this.state.count++; +} +``` + +### useState in Template + +```xml +
+ +``` + +### Computed State + +```javascript +setup() { + this.state = useState({count: 0}); + this.double = computed(() => this.state.count * 2); +} +``` + +--- + +## QWeb Templates + +### Basic Template + +```xml + + + +
+

+

+

+
+
+``` + +### Event Handlers + +```xml + + +``` + +### Loops and Conditions + +```xml + +
+ +
+ + +
Visible when true
+
Visible when false
+``` + +--- + +## Translations + +### Translate in JavaScript + +```javascript +import { _t } from "@web/core/l10n/translation"; + +this.message = _t("Hello World"); +``` + +### Translate with Parameters + +```javascript +this.message = _t("Hello %(name)s", { name: "John" }); +``` + +### Translate in Template + +```xml + +``` + +--- + +## Examples + +### Counter Component + +```javascript +import { Component, useState } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; + +export class Counter extends Component { + static template = "my_module.Counter"; + + setup() { + this.state = useState({ count: 0 }); + } + + increment() { + this.state.count++; + } + + decrement() { + this.state.count--; + } +} +``` + +```xml + + +
+ + + +
+
+
+``` + +### Data Loading Component + +```javascript +import { Component, useState, onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class DataComponent extends Component { + static template = "my_module.DataComponent"; + + setup() { + this.orm = useService("orm"); + this.state = useState({ + records: [], + loading: true, + }); + + onWillStart(this.loadData); + } + + async loadData() { + this.state.records = await this.orm.search("my.model", [], { + limit: 10, + }); + this.state.loading = false; + } +} +``` + +--- + +## Best Practices + +### Use Hooks for Side Effects + +```javascript +setup() { + onMounted(() => { + // Side effects here + }); +} +``` + +### Cleanup Resources + +```javascript +setup() { + onWillUnmount(() => { + // Cleanup here + }); +} +``` + +### Avoid Direct DOM Manipulation + +Use templates and reactive state instead. + +### Split Components + +Keep components small and focused. + +```javascript +// Good: Small focused component +export class UserName extends Component { + static template = "my_module.UserName"; +} + +// Bad: Large monolithic component +export class Everything extends Component { + static template = "my_module.Everything"; +} +``` + +--- + +## References + +- OWL documentation +- Odoo 19 Web framework docs diff --git a/.agents/skills/odoo-19/references/odoo-19-performance-guide.md b/.agents/skills/odoo-19/references/odoo-19-performance-guide.md new file mode 100644 index 00000000..b8dd61db --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-performance-guide.md @@ -0,0 +1,357 @@ +# Odoo 19 Performance Guide + +Guide for optimizing Odoo 19 code: preventing N+1 queries, reducing database queries, and using profiler. + +## Table of Contents + +- [Profiling](#profiling) +- [Batch Operations](#batch-operations) +- [Algorithmic Complexity](#algorithmic-complexity) +- [Indexes](#indexes) +- [Performance Pitfalls](#performance-pitfalls) + +--- + +## Profiling + +Odoo provides an integrated profiling tool to record SQL queries and stack traces. + +### Enable from User Interface + +1. Enable developer mode +2. Toggle **Enable profiling** button +3. Choose expiry time +4. Toggle **Enable profiling** again to start session profiling + +Options: + +- **Record sql** - Saves all SQL queries with stack trace +- **Record traces** - Saves stack trace periodically (default: 10ms interval) + +### Enable from Python Code + +```python +from odoo.tools.profiler import Profiler + +# Basic profiling +with Profiler(): + do_stuff() + +# With custom collectors +with Profiler(collectors=['sql', PeriodicCollector(interval=0.1)]): + do_stuff() + +# In tests +with self.profile(): + do_stuff() +``` + +### Collectors + +| Collector | Key | Description | +| ------------------ | -------------- | ------------------------------------------------ | +| SQL collector | `sql` | Saves SQL queries with stack trace | +| Periodic collector | `traces_async` | Saves stack trace periodically (separate thread) | +| QWeb collector | `qweb` | Saves QWeb directive execution | +| Sync collector | `traces_sync` | Saves every function call/return (high overhead) | + +### Execution Context + +Add context to identify calls in speedscope: + +```python +for index in range(max_index): + with ExecutionContext(current_index=index): + do_stuff() +``` + +### Performance Pitfalls + +- Randomness can lead to different results (garbage collector, etc.) +- Blocking calls may cause unexpected long frames +- Cache state affects results (view/assets in cache) +- Profiler overhead can impact performance (especially SQL collector) +- Large profiles may cause memory issues + +--- + +## Batch Operations + +### Avoid Loop Queries + +**BAD**: Search in loop (N queries) + +```python +def _compute_count(self): + for record in self: + domain = [('related_id', '=', record.id)] + record.count = other_model.search_count(domain) +``` + +**GOOD**: Use `_read_group` (1 query) + +> **Odoo 19**: `read_group()` is **deprecated**. Use `_read_group()` (internal) or `formatted_read_group()` (public API). + +```python +def _compute_count(self): + domain = [('related_id', 'in', self.ids)] + counts_data = other_model._read_group(domain, ['related_id'], ['__count']) + mapped_data = {r['related_id'][0]: r['__count'] for r in counts_data} + for record in self: + record.count = mapped_data.get(record.id, 0) +``` + +### Batch Creates + +**BAD**: Create in loop + +```python +for name in ['foo', 'bar']: + model.create({'name': name}) +``` + +**GOOD**: Batch create + +```python +create_values = [{'name': name} for name in ['foo', 'bar']] +records = model.create(create_values) +``` + +### Prefetch Records + +**BAD**: Browse one at a time + +```python +for record_id in record_ids: + record = model.browse(record_id) + record.foo # One query per record +``` + +**GOOD**: Browse all together + +```python +records = model.browse(record_ids) +for record in records: + record.foo # One query for entire recordset +``` + +### Disable Prefetching (when needed) + +```python +for values in values_list: + message = self.browse(values['id']).with_prefetch(self.ids) +``` + +--- + +## Algorithmic Complexity + +### Reduce Nested Loops + +**BAD**: O(n²) complexity + +```python +for record in self: + for result in results: + if result['id'] == record.id: + record.foo = result['foo'] + break +``` + +**GOOD**: Use dictionary (O(n)) + +```python +mapped_result = {result['id']: result['foo'] for result in results} +for record in self: + record.foo = mapped_result.get(record.id) +``` + +### Use Set Operations + +**BAD**: List-like `in` check (quadratic) + +```python +invalid_ids = self.search(domain).ids +for record in self: + if record.id in invalid_ids: # O(n) for each record + ... +``` + +**GOOD**: Use set (O(n) total) + +```python +invalid_ids = set(self.search(domain).ids) +for record in self: + if record.id in invalid_ids: + ... +``` + +**ALTERNATIVE**: Recordset operations + +```python +invalid_ids = self.search(domain) +for record in self - invalid_ids: + ... +``` + +--- + +## Indexes + +Database indexes speed up search operations. + +```python +name = fields.Char(string="Name", index=True) +``` + +**Warning**: Don't index every field - indexes consume space and impact INSERT/UPDATE/DELETE performance. + +### Using Indexes + +```python +# Field-level index +name = fields.Char(index=True) +records = self.search([('name', '=', 'value')]) # Uses index scan +``` + +### Declarative Index (Odoo 19) + +For composite indexes, use `models.Index` as a model attribute: + +```python +class MyModel(models.Model): + _name = 'my.model' + + name = fields.Char() + code = fields.Char() + + _name_code_idx = models.Index('(name, code)') +``` + +--- + +## Performance Pitfalls + +### N+1 Query Problem + +Occurs when you: + +1. Fetch a list of records +2. Loop through them +3. Execute a query for each record + +**Detection**: Use `--log-sql` CLI parameter or profiler + +**Solution**: Fetch related data in one query using: + +- `_read_group()` +- `search_fetch()` +- `fetch()` +- `mapped()` + +### Large Recordsets + +Processing large recordsets can cause memory issues. + +**Solution**: Process in batches + +```python +def _process_large_dataset(self): + limit = 1000 + offset = 0 + while True: + records = self.search([], limit=limit, offset=offset) + if not records: + break + records.process() + offset += limit +``` + +### Computed Field Dependencies + +Missing dependencies cause recomputation at wrong time. + +**BAD**: Missing dotted dependency + +```python +@api.depends('partner_id') +def _compute_email(self): + for record in self: + record.email = record.partner_id.email # N queries! +``` + +**GOOD**: Include dotted path + +```python +@api.depends('partner_id.email') +def _compute_email(self): + for record in self: + record.email = record.partner_id.email # 1 query for all +``` + +### Context Pollution + +Excessive context changes can cause issues. + +```python +# BAD: Too many with_context calls +records.with_context(lang='fr').with_context(active_test=False).with_context(...) +``` + +**Solution**: Consolidate context changes + +```python +# GOOD: Single with_context call +records.with_context(lang='fr', active_test=False) +``` + +### Unnecessary invalidate_cache + +Calling `invalidate_cache()` too frequently defeats the purpose of caching. + +**Solution**: Only invalidate fields that actually changed + +```python +# GOOD: Invalidate only changed fields +records.invalidate_recordset(['field1', 'field2']) +``` + +--- + +## Query Count Testing + +Use `assertQueryCount` in tests to establish query limits. + +```python +with self.assertQueryCount(11): + do_something() +``` + +Combine with profiler for analysis: + +```python +with self.profile(): + with self.assertQueryCount(__system__=1211): + do_stuff() +``` + +--- + +## Good Practices Summary + +| Practice | Description | +| -------------------------- | --------------------------------------------- | +| **Batch operations** | Accumulate operations, execute in batch | +| **Use \_read_group** | Replace search/search_count in loops | +| **Prefetch records** | Browse all records together | +| **Reduce complexity** | Use dictionaries/sets instead of nested loops | +| **Add indexes** | On frequently searched fields | +| **Use fetch/search_fetch** | For targeted data loading | +| **Profile first** | Use profiler before optimizing | +| **Test query counts** | Use assertQueryCount in tests | + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/performance.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-reports-guide.md b/.agents/skills/odoo-19/references/odoo-19-reports-guide.md new file mode 100644 index 00000000..6f3172eb --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-reports-guide.md @@ -0,0 +1,343 @@ +# Odoo 19 Reports Guide + +Guide for creating QWeb reports in Odoo 19: PDF/HTML reports, templates, and paper formats. + +## Table of Contents + +- [Report Basics](#report-basics) +- [Report Declaration](#report-declaration) +- [Report Templates](#report-templates) +- [Custom Reports](#custom-reports) +- [Paper Formats](#paper-formats) +- [Translatable Reports](#translatable-reports) +- [Barcodes](#barcodes) + +--- + +## Report Basics + +Reports are written in HTML/QWeb. PDF rendering is performed by wkhtmltopdf. + +Reports consist of: + +1. A **report action** (`ir.actions.report`) +2. A **QWeb template** for the report content + +--- + +## Report Declaration + +### Using report Tag (Simple) + +```xml + +``` + +### Using record Tag (Advanced) + +```xml + + My Report + my.model + qweb-pdf + my_module.my_template + 'My Report - %s' % (object.name) + + + +``` + +### Report Attributes + +| Attribute | Description | +| ------------------- | ----------------------------------------- | +| `id` | External identifier | +| `string` | Report name | +| `model` | Model for the report | +| `report_type` | `qweb-pdf` or `qweb-html` | +| `name` | External ID of QWeb template | +| `file` | Same as name (for backward compatibility) | +| `print_report_name` | Python expression for filename | +| `paperformat_id` | Paper format | +| `binding_model_id` | Model to show in Print menu | +| `groups_id` | Groups allowed to use report | +| `multi` | If True, don't show on form view | +| `attachment_use` | Generate once, reprint from stored | +| `attachment` | Python expression for attachment | + +--- + +## Report Templates + +### Template Variables + +Report templates always have access to: + +| Variable | Description | +| ------------------- | --------------------------------------------- | +| `docs` | Records for the current report | +| `doc_ids` | List of ids for `docs` | +| `doc_model` | Model name for `docs` | +| `time` | Python `time` module | +| `user` | User printing the report | +| `res_company` | Current user's company | +| `website` | Current website (if any) | +| `web_base_url` | Base URL for webserver | +| `context_timestamp` | Function to convert datetime to user timezone | + +### Minimal Template + +```xml + +``` + +### External Layout + +Calling `web.external_layout` adds default header and footer. + +```xml + +
+ +
+
+``` + +#### Layout Variants + +| Layout | Description | +| -------------------------------- | ------------------------------------ | +| `web.external_layout` | Standard layout (with header/footer) | +| `web.external_layout_background` | With background | +| `web.external_layout_clean` | Clean layout (minimal) | +| `web.external_layout_boxed` | Boxed layout | + +--- + +## Custom Reports + +For custom reports, create a report class that overrides `_get_report_values`: + +```python +from odoo import models + +class MyReport(models.AbstractModel): + _name = 'report.my_module.my_template' + _description = 'My Custom Report' + + def _get_report_values(self, docids, data=None): + docs = self.env['my.model'].browse(docids) + + return { + 'doc_ids': docids, + 'doc_model': 'my.model', + 'docs': docs, + 'data': data, + 'my_custom_value': self._compute_custom(docs), + } + + def _compute_custom(self, docs): + # Custom computation + return sum(doc.amount for doc in docs) +``` + +Use custom values in template: + +```xml + +``` + +--- + +## Paper Formats + +### Built-in Paper Formats + +| Format | Size | +| ---------------------- | ------ | +| `paperformat_euro` | A4 | +| `paperformat_us` | Letter | +| `paperformat_us_legal` | Legal | + +### Custom Paper Format + +```xml + + My Custom Format + + A4 + 297 + 210 + Portrait + 40 + 20 + 7 + 7 + + 35 + 90 + +``` + +### Format Attributes + +| Attribute | Description | +| ---------------- | --------------------------------------- | +| `format` | Paper size (A4, Letter, etc.) or Custom | +| `page_height` | Page height in mm | +| `page_width` | Page width in mm | +| `orientation` | Portrait or Landscape | +| `margin_top` | Top margin in mm | +| `margin_bottom` | Bottom margin in mm | +| `margin_left` | Left margin in mm | +| `margin_right` | Right margin in mm | +| `header_line` | Show header line | +| `header_spacing` | Header spacing in mm | +| `dpi` | Resolution (default: 90) | + +--- + +## Translatable Reports + +### Two-Template Approach + +1. Main template (wrapper) +2. Translatable document template + +```xml + + + + + +``` + +### Language Attribute + +Use `t-lang` to set language for a template section: + +```xml + + + + + + + +``` + +**Note**: Works only with `t-call`, not on regular XML nodes. + +--- + +## Barcodes + +### Embed Barcode + +```xml + +``` + +### Barcode Controller Parameters + +| Parameter | Values | +| --------------- | ------------------------------------------------ | +| `type` | `QR`, `Code128`, `EAN13`, `EAN8`, `UPCA`, `UPCE` | +| `value` | Barcode value | +| `width` | Width in pixels | +| `height` | Height in pixels | +| `humanreadable` | `1` or `0` (show text below) | + +### Example + +```xml +
+

Product:

+ Barcode +
+``` + +--- + +## QWeb Tips + +### Directives + +| Directive | Description | +| ----------- | ----------------------------------------------------- | +| `t-out` | Escape and output value (replaces deprecated `t-esc`) | +| `t-field` | Output field value (formatted) | +| `t-if` | Conditional display | +| `t-elif` | Else if condition | +| `t-else` | Else condition | +| `t-foreach` | Loop over collection | +| `t-as` | Variable name for foreach | +| `t-set` | Set variable | +| `t-call` | Call another template | +| `t-lang` | Set language for template | + +### t-field Options + +```xml + + +
+``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/reports.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-security-guide.md b/.agents/skills/odoo-19/references/odoo-19-security-guide.md new file mode 100644 index 00000000..9d3e006f --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-security-guide.md @@ -0,0 +1,381 @@ +# Odoo 19 Security Guide + +Guide for Odoo 19 security: access rights, record rules, field permissions, and security pitfalls. + +## Table of Contents + +- [Security Overview](#security-overview) +- [Groups](#groups) +- [Access Rights (ACL)](#access-rights-acl) +- [Record Rules](#record-rules) +- [Field Access](#field-access) +- [Security Pitfalls](#security-pitfalls) + +--- + +## Security Overview + +Odoo provides two main data-driven mechanisms to manage access: + +1. **Access Rights (ACL)** - Grants access to an entire model for operations +2. **Record Rules** - Conditions that must be satisfied for operations + +Both are linked to users through **groups**. + +--- + +## Groups + +`res.groups` - Users belong to groups, security mechanisms are associated to groups. + +> **Odoo 19 Breaking Change**: `category_id` has been **removed** from `res.groups`. Replaced by a new **privilege-based system** using `res.groups.privilege`. + +### Key Attributes + +| Attribute | Description | +| -------------- | ------------------------------------------------------------------------- | +| `name` | User-readable identification (role/purpose) | +| `privilege_id` | **Odoo 19**: Reference to `res.groups.privilege` (replaces `category_id`) | +| `implied_ids` | Other groups to set on the user alongside this one | +| `comment` | Additional notes | + +### 3-Tier Security Architecture (Odoo 19) + +``` +ir.module.category → res.groups.privilege → res.groups → res.users +``` + +### Example + +```xml + + + My Module Access + + + + + + My Module User + + + Users can access my module features. + +``` + +> **Migration**: Replace `category_id` with `privilege_id` in `security.xml` and create corresponding `res.groups.privilege` records. + +--- + +## Access Rights (ACL) + +Access rights **grant** access to an entire model for operations. If no access rights matches an operation, the user doesn't have access. + +Access rights are **additive** - a user's accesses are the union of all groups. + +`ir.model.access` - Access control list entries. + +### Key Attributes + +| Attribute | Description | +| ------------- | ----------------------------------------- | +| `name` | Purpose or role of the group | +| `model_id` | Model whose access the ACL controls | +| `group_id` | Group granted access (empty = every user) | +| `perm_create` | Grant create access | +| `perm_read` | Grant read access | +| `perm_write` | Grant write access | +| `perm_unlink` | Grant unlink (delete) access | + +### Example CSV (Common Format) + +File: `security/ir.model.access.csv` + +```csv +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_my_model_user,my.model.user,model_my_model,group_my_module_user,1,1,0,0 +access_my_model_manager,my.model.manager,model_my_model,group_my_module_manager,1,1,1,1 +``` + +### Example XML + +```xml + + my.model.user + + + + + + + +``` + +--- + +## Record Rules + +Record rules are **conditions** which must be satisfied for operations. They are evaluated record-by-record, following access rights. + +Record rules are **default-allow**: if access rights grant access and no rule applies, access is granted. + +`ir.rule` - Record rules. + +### Key Attributes + +| Attribute | Description | +| ------------------------------------------------------- | -------------------------------------------------- | +| `name` | Description of the rule | +| `model_id` | Model to which the rule applies | +| `groups` | Groups to which access is granted (empty = global) | +| `domain_force` | Domain predicate (Python expression) | +| `perm_read`, `perm_write`, `perm_create`, `perm_unlink` | Operations the rule applies to (all by default) | + +### Domain Force Variables + +| Variable | Description | +| ------------- | ----------------------------------------------- | +| `time` | Python's `time` module | +| `user` | Current user (singleton recordset) | +| `company_id` | Current user's selected company (single id) | +| `company_ids` | All companies the user has access (list of ids) | + +### Example: Multi-Company Rule + +```xml + + My Module: multi-company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + +``` + +### Example: User-Only Records + +```xml + + My Module: user records + + [('user_id', '=', user.id)] + + +``` + +### Global vs Group Rules + +There's a large difference between global and group rules: + +| Rule Type | Behavior | +| ---------------------- | -------------------------------------------------- | +| **Global** (no groups) | **Intersect** - all global rules must be satisfied | +| **Group** rules | **Unify** - any group rule can be satisfied | +| **Global + Group** | **Intersect** - first group rule restricts access | + +> **Danger**: Creating multiple global rules is risky - possible to create non-overlapping rulesets which will remove all access. + +--- + +## Field Access + +An ORM field can have a `groups` attribute providing a list of groups (comma-separated external identifiers). + +If the current user is not in one of the listed groups: + +- Restricted fields are removed from views +- Restricted fields are removed from `fields_get()` responses +- Attempts to read/write restricted fields result in an access error + +```python +notes = fields.Text('Internal Notes', groups='base.group_system') + +# Only users in base.group_system can see/edit this field +``` + +--- + +## Security Pitfalls + +### Unsafe Public Methods + +Any public method can be executed via RPC call. Methods starting with `_` are not callable from action buttons or external API. + +> **Odoo 19**: Use `@api.private` decorator to explicitly prevent RPC access on any method. + +```python +# BAD: this method is public and arguments can not be trusted +def action_done(self): + if self.state == "draft" and self.env.user.has_group('base.manager'): + self._set_state("done") + +# GOOD: use @api.private (Odoo 19) or underscore prefix +@api.private +def _set_state(self, new_state): + self.sudo().write({"state": new_state}) +``` + +### Bypassing the ORM + +**Never** use the database cursor directly when the ORM can do the same thing! + +#### Wrong (very bad) + +```python +# SQL injection vulnerability +self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + + ','.join(map(str, ids)) + ') AND state=%s AND obj_price > 0', + ('draft',)) +``` + +#### Better (no injection, but still wrong) + +```python +self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s ' + 'AND state=%s AND obj_price > 0', + (tuple(ids), 'draft',)) +``` + +#### Best (use ORM) + +```python +auction_lots_ids = self.search([ + ('auction_id', 'in', ids), + ('state', '=', 'draft'), + ('obj_price', '>', 0) +]).ids +``` + +#### Using SQL class (recommended) + +```python +from odoo.tools import SQL + +auction_lots_ids = self.env.execute_query(SQL(""" + SELECT id FROM auction_lots + WHERE auction_id IN %s AND state = %s AND obj_price > 0 +""", tuple(ids), 'draft')) +``` + +### SQL Injections + +**Never** use Python string concatenation (+) or interpolation (%) for SQL queries. + +Use psycopg2 parameter passing or `odoo.tools.SQL` wrapper. + +```python +# BAD: SQL injection vulnerability +self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' + 'WHERE parent_id IN ('+','.join(map(str, ids))+')') + +# GOOD: use parameter passing +self.env.cr.execute('SELECT DISTINCT child_id ' + 'FROM account_account_consol_rel ' + 'WHERE parent_id IN %s', + (tuple(ids),)) + +# BETTER: use SQL wrapper +from odoo.tools import SQL +self.env.cr.execute(SQL(""" + SELECT DISTINCT child_id + FROM account_account_consol_rel + WHERE parent_id IN %s +""", tuple(ids))) +``` + +### Building Domains + +Use `odoo.fields.Domain` to handle domain manipulation safely. + +```python +# BAD: the user can pass ['|', ('id', '>', 0)] to access all +domain = ... # passed by user +security_domain = [('user_id', '=', self.env.uid)] +domain += security_domain # can have side effects +self.search(domain) + +# GOOD: use Domain class +from odoo.fields import Domain +domain = Domain(...) +domain &= Domain('user_id', '=', self.env.uid) +self.search(domain) +``` + +### Unescaped Field Content + +Avoid using `t-raw` for rich-text content - it's an XSS vector. + +#### Bad XML + +```xml +
+
+
+``` + +#### Good XML + +```xml +
+
+
+
+``` + +### Escaping vs Sanitizing + +**Escaping** (always mandatory) converts TEXT to CODE. + +```python +from markupsafe import Markup + +# data is TEXT +code = html_escape(data) # Convert to CODE +self.website_description = Markup("%s") % code +``` + +**Sanitizing** converts CODE to SAFER CODE (but not necessarily safe). + +```python +from odoo.tools import html_sanitize + +code = Markup("

Important

") +html_sanitize(code, strip_classes=True) # => Markup('

Important

') +``` + +### Evaluating Content + +Avoid `eval()` - use `ast.literal_eval()` for parsing. + +```python +# BAD: very bad +domain = eval(self.filter_domain) + +# BETTER: still not recommended +from odoo.tools import safe_eval +domain = safe_eval(self.filter_domain) + +# GOOD: use literal_eval +from ast import literal_eval +domain = literal_eval(self.filter_domain) +``` + +### Accessing Object Attributes + +Use `__getitem__` instead of `getattr()` for dynamic field access. + +```python +# BAD: unsafe, can access any attribute +def _get_state_value(self, res_id, state_field): + record = self.sudo().browse(res_id) + return getattr(record, state_field, False) + +# GOOD: safer +def _get_state_value(self, res_id, state_field): + record = self.sudo().browse(res_id) + return record[state_field] +``` + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/security.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-testing-guide.md b/.agents/skills/odoo-19/references/odoo-19-testing-guide.md new file mode 100644 index 00000000..6aa47494 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-testing-guide.md @@ -0,0 +1,481 @@ +# Odoo 19 Testing Guide + +Guide for testing Odoo 19: Python unit tests, JS tests, and tours (integration tests). + +## Table of Contents +- [Test Types](#test-types) +- [Python Tests](#python-tests) +- [Test Classes](#test-classes) +- [Test Decorators](#test-decorators) +- [Test Selection](#test-selection) +- [JS Tests](#js-tests) +- [Integration Tests (Tours)](#integration-tests-tours) +- [Performance Testing](#performance-testing) + +--- + +## Test Types + +Odoo has three kinds of tests: + +| Type | Purpose | +|------|---------| +| **Python unit tests** | Test model business logic | +| **JS unit tests** | Test JavaScript code in isolation | +| **Tours** | Integration testing (Python + JS together) | + +--- + +## Python Tests + +### Test Structure + +Create a `tests` sub-package in your module: + +``` +your_module/ +├── ... +├── tests/ +│ ├── __init__.py +│ ├── test_bar.py +│ └── test_foo.py +``` + +**Important**: Import test modules from `tests/__init__.py` + +```python +# tests/__init__.py +from . import test_foo, test_bar +``` + +### Test Methods + +Test methods must start with `test_`: + +```python +class TestModelA(TransactionCase): + def test_some_action(self): + record = self.env['model.a'].create({'field': 'value'}) + record.some_action() + self.assertEqual(record.field, expected_value) +``` + +--- + +## Test Classes + +### TransactionCase + +Most common test class. Each test runs in its own transaction, rolled back at the end. + +```python +from odoo.tests import TransactionCase + +class TestMyModel(TransactionCase): + def test_create_record(self): + record = self.env['my.model'].create({'name': 'Test'}) + self.assertTrue(record.id) + self.assertEqual(record.name, 'Test') +``` + +### SingleTransactionCase + +Runs all tests in a single transaction (not rolled back). Faster, but tests can affect each other. + +```python +from odoo.tests import SingleTransactionCase + +class TestMyModel(SingleTransactionCase): + def test_1_first(self): + # Creates data + pass + + def test_2_second(self): + # Can use data from test_1_first + pass +``` + +### HttpCase + +For web-related tests. Starts a browser (headless Chrome by default). + +```python +from odoo.tests import HttpCase + +class TestWebsite(HttpCase): + def test_homepage(self): + self.url_open('/hello') +``` + +--- + +## Test Decorators + +### tagged + +Add or remove tags from test classes. + +```python +from odoo.tests import TransactionCase, tagged + +@tagged('-standard', 'nice') +class NiceTest(TransactionCase): + pass +``` + +### Default Tags + +- `standard` - Default tag (selected by `--test-tags`) +- `at_install` - Run after module installation (default) +- `post_install` - Run after all modules installed + +```python +@tagged('-at_install', 'post_install') +class WebsiteVisitorTests(HttpCase): + def test_create_visitor(self): + pass +``` + +--- + +## Test Selection + +### By Tag + +```bash +# Run only nice tests +odoo-bin --test-tags nice + +# Run nice and standard tests +odoo-bin --test-tags nice,standard + +# Run standard except slow +odoo-bin --test-tags standard,-slow +``` + +### By Module + +```bash +# Run only sale module tests +odoo-bin --test-tags /sale + +# Run sale module but not slow tests +odoo-bin --test-tags '/sale,-slow' + +# Run stock or slow tests +odoo-bin --test-tags '-standard, slow, /stock' +``` + +### By Specific Test + +```bash +# Run specific test function +odoo-bin --test-tags .test_supplier_invoice + +# Equivalent (full path) +odoo-bin --test-tags /account:TestAccount.test_supplier_invoice +``` + +### Tag Format + +``` +[-][tag][/module][:class][.method] +``` + +| Prefix | Meaning | +|--------|---------| +| `-` | Deselect/remove tag | +| `+` | Select/add tag (implicit, optional) | +| `/module` | Specific module | +| `:class` | Specific class | +| `.method` | Specific method | + +--- + +## Test Utilities + +### browse_ref + +Browse an external ID: + +```python +record = self.browse_ref('base.user_admin') +self.assertEqual(record.login, 'admin') +``` + +### ref + +Get database ID from external ID: + +```python +user_id = self.ref('base.user_admin') +``` + +### Form + +Helper for testing form views: + +```python +from odoo.tests import Form + +with Form(self.env['sale.order']) as form: + form.partner_id = self.partner + form.date_order = '2023-01-01' + with form.order_line.new() as line: + line.product_id = self.product + line.product_uom_qty = 10 + +order = form.save() +``` + +--- + +## JS Tests + +Odoo uses Hoot for JS unit testing. See the frontend testing documentation. + +Test files go in `static/tests/`: + +``` +your_module/ +└── static/ + └── tests/ + └── my_test.js +``` + +```javascript +import { start } from '@mail/utils/test_utils'; + +QUnit.module('My Module', { + beforeEach() { + this.data = { + records: { + 'my.model': [{id: 1, name: 'Test'}], + }, + }; + }, +}, function () { + QUnit.test('my test', async function (assert) { + // Test code here + }); +}); +``` + +--- + +## Integration Tests (Tours) + +Tours simulate real user scenarios in the browser. + +### Tour Structure + +``` +your_module/ +├── static/ +│ └── tests/ +│ └── tours/ +│ └── my_tour.js +├── tests/ +│ ├── __init__.py +│ └── test_my_tour.py +└── __manifest__.py +``` + +### Register Tour (JavaScript) + +```javascript +import tour from 'web_tour.tour'; + +tour.register('my_tour', { + url: '/web', +}, [ + // Step 1: Show apps menu + tour.stepUtils.showAppsMenuItem(), + // Step 2: Click on app + { + trigger: '.o_app[data-menu-xmlid="my_module.menu_root"]', + run: "click", + }, + // Step 3: Fill form + { + trigger: 'input[name="name"]', + run: "text", + }, + // Step 4: Verify + { + trigger: '.o_data_row:first', + run: function () { + // Assertions here + }, + }, +]); +``` + +### Add to Manifest + +```python +'assets': { + 'web.assets_tests': [ + 'your_module/static/tests/tours/my_tour.js', + ], +}, +``` + +### Start Tour (Python) + +```python +from odoo.tests import HttpCase + +class TestMyTour(HttpCase): + def test_my_tour(self): + self.start_tour("/web", "my_tour", login="admin") +``` + +### Tour Step Options + +| Option | Description | +|--------|-------------| +| `trigger` | Selector/element to run action on | +| `run` | Action to perform (see helpers below) | +| `isActive` | Array of conditions (mobile, enterprise, auto/manual) | +| `content` | Tooltip content | +| `tooltipPosition` | `top`, `right`, `bottom`, or `left` | +| `timeout` | Wait time in ms (default: 10000) | + +### Run Actions + +| Action | Description | +|--------|-------------| +| `click` | Clicks the element | +| `dblclick` | Double-clicks the element | +| `drag_and_drop {target}` | Drags to target | +| `edit {content}` | Clears and fills | +| `editor {content}` | WYSIWYG editor fill | +| `fill {content}` | Fills the element | +| `hover` | Hovers over element | +| `press {content}` | Keyboard input | +| `range {content}` | Range slider value | +| `select {value}` | Select by value | +| `selectByIndex {index}` | Select by index | +| `selectByLabel {label}` | Select by label | +| `check` | Checks checkbox | +| `uncheck` | Unchecks checkbox | +| `clear` | Clears input | + +### Run Tour from Browser + +```javascript +odoo.startTour("tour_name"); +``` + +Or use `?debug=tests` URL parameter. + +### Debug Tours + +```python +# Watch mode (opens Chrome window) +self.start_tour("/web", "my_tour", watch=True) + +# Debug mode (opens devtools) +self.start_tour("/web", "my_tour", debug=True) +``` + +### Onboarding Tours + +Onboarding tours are interactive guides for users. + +Create `web_tour.tour` record: + +```xml + + my_tour + 10 + Great job! + +``` + +Add to manifest: + +```python +'data': [ + 'data/my_tour.xml', +], +'assets': { + 'web.assets_backend': [ + 'your_module/static/src/js/tours/my_tour.js', + ], +}, +``` + +--- + +## Performance Testing + +### Query Count Testing + +Use `assertQueryCount` to establish query limits: + +```python +with self.assertQueryCount(11): + do_something() +``` + +### Per-System Limits + +```python +with self.assertQueryCount(__system__=1211): + do_something() +``` + +--- + +## Running Tests + +Enable tests when starting Odoo: + +```bash +# Enable tests +odoo-bin --test-enable + +# With specific tags +odoo-bin --test-tags post_install + +# Specific module +odoo-bin -i my_module --test-enable + +# Update and test +odoo-bin -u my_module --test-enable +``` + +--- + +## Screenshot and Screencast + +When `HttpCase.browser_js` tests fail: + +- Screenshot saved to: `/tmp/odoo_tests/{db_name}/screenshots/` + +CLI options: + +```bash +odoo-bin --screenshots /tmp/screenshots +odoo-bin --screencasts /tmp/screencasts +``` + +--- + +## Special Tags Reference + +| Tag | Description | +|-----|-------------| +| `standard` | Default tag for BaseCase subclasses | +| `at_install` | Run after module installation (default) | +| `post_install` | Run after all modules installed | +| `-standard` | Remove from default | +| `-at_install` | Don't run at install (use with `post_install`) | + +--- + +## References + +- Source: Odoo 19 documentation `/doc/developer/reference/backend/testing.rst` diff --git a/.agents/skills/odoo-19/references/odoo-19-transaction-guide.md b/.agents/skills/odoo-19/references/odoo-19-transaction-guide.md new file mode 100644 index 00000000..41587bc4 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-transaction-guide.md @@ -0,0 +1,261 @@ +# Odoo 19 Transaction Guide + +Guide for handling database transactions in Odoo 19: errors, savepoints, and serialization failures. + +## Table of Contents +- [Transaction Overview](#transaction-overview) +- [Database Errors](#database-errors) +- [Savepoints](#savepoints) +- [Error Handling](#error-handling) +- [Serialization Failures](#serialization-failures) +- [Best Practices](#best-practices) + +--- + +## Transaction Overview + +Odoo uses database transactions to ensure data consistency. + +### Transaction Properties + +| Property | Description | +|-----------|-------------| +| **Atomicity** | All or nothing | +| **Consistency** | Data remains valid | +| **Isolation** | Concurrent transactions don't interfere | +| **Durability** | Committed data persists | + +### Transaction Flow + +``` +Begin Transaction +├── Execute Operations +├── (Commit or Rollback) +└── End Transaction +``` + +--- + +## Database Errors + +### Common Errors + +| Error | When | +|-------|------| +| `UniqueViolation` | Duplicate unique constraint | +| `NotNullViolation` | NULL in NOT NULL column | +| `ForeignKeyViolation` | Invalid foreign key | +| `CheckViolation` | CHECK constraint failed | +| `SerializationFailure` | Concurrent modification | + +### Catch Database Errors + +```python +from odoo.exceptions import ValidationError, UserError +from psycopg2 import errors + +try: + record.write({'field': 'value'}) +except errors.UniqueViolation as e: + raise ValidationError("Duplicate value!") +except errors.NotNullViolation as e: + raise ValidationError("Required field missing!") +``` + +--- + +## Savepoints + +Savepoints isolate errors within a transaction. + +### Using Savepoints + +```python +def process_records(self): + for record in self: + # Create savepoint before each record + self.env.cr.execute("SAVEPOINT my_savepoint") + + try: + record.process() + except Exception as e: + # Rollback to savepoint on error + self.env.cr.execute("ROLLBACK TO SAVEPOINT my_savepoint") + _logger.warning("Failed to process %s: %s", record, e) +``` + +### Release Savepoint + +```python +try: + record.process() +finally: + # Release savepoint + self.env.cr.execute("RELEASE SAVEPOINT my_savepoint") +``` + +--- + +## Error Handling + +### Retry on Serialization Failure + +```python +from odoo.exceptions import UserError +from psycopg2 import OperationalError + +def retry_on_failure(max_retries=3): + def decorator(func): + def wrapper(self, *args, **kwargs): + for attempt in range(max_retries): + try: + return func(self, *args, **kwargs) + except OperationalError as e: + if e.pgcode == '40001': # Serialization failure + if attempt < max_retries - 1: + self.env.cr.rollback() + self.env.cr.execute("SAVEPOINT retry_savepoint") + continue + raise + return wrapper + return decorator +``` + +### Handle Validation Errors + +```python +from odoo.exceptions import ValidationError + +@api.constrains('email') +def _check_email(self): + for record in self: + if not tools.email_validation(record.email): + raise ValidationError("Invalid email: %s" % record.email) +``` + +--- + +## Serialization Failures + +### What is Serialization Failure? + +Occurs when two transactions try to modify the same data concurrently. + +### Avoid Serialization Failures + +```python +# BAD: Loop with search and write +def process(self): + for record in self.search([('state', '=', 'draft')]): + record.write({'state': 'done'}) + +# GOOD: Single write +def process(self): + self.search([('state', '=', 'draft')]).write({'state': 'done'}) +``` + +### Use SQL FOR UPDATE + +```python +self.env.cr.execute("SELECT id FROM my_model WHERE id IN %s FOR UPDATE", (tuple(self.ids),)) +# Process records +``` + +--- + +## Commit and Rollback + +### Auto Commit + +Odoo auto-commits after successful operations. + +```python +# Transaction is auto-committed +record.write({'field': 'value'}) +``` + +### Manual Rollback + +```python +try: + # Multiple operations + record1.write({'field': 'value'}) + record2.write({'field': 'value'}) +except Exception as e: + # Rollback entire transaction + self.env.cr.rollback() + raise +``` + +--- + +## Best Practices + +### Batch Operations + +```python +# GOOD: Batch create +def create_records(self, values_list): + return self.create(values_list) + +# BAD: Create in loop +def create_records(self, values_list): + for values in values_list: + self.create(values) +``` + +### Use Context for Special Cases + +```python +# Skip tracking for bulk update +records.with_context(tracking_disable=True).write({'field': 'value'}) +``` + +### Validate Before Writing + +```python +@api.constrains('email') +def _check_email(self): + # Validate before write + for record in self: + if not tools.email_validation(record.email): + raise ValidationError("Invalid email") +``` + +--- + +## Common Patterns + +### Safe Update Pattern + +```python +def safe_update(self, values): + try: + self.write(values) + except errors.UniqueViolation: + raise UserError("Duplicate entry!") + except errors.NotNullViolation: + raise UserError("Required field missing!") +``` + +### Bulk Processing with Savepoints + +```python +def bulk_process(self, records): + for record in records: + self.env.cr.execute("SAVEPOINT process_savepoint") + try: + record.process() + except Exception as e: + self.env.cr.execute("ROLLBACK TO SAVEPOINT process_savepoint") + _logger.warning("Failed: %s", e) + finally: + self.env.cr.execute("RELEASE SAVEPOINT process_savepoint") +``` + +--- + +## References + +- PostgreSQL documentation on transactions +- Odoo 19 ORM documentation diff --git a/.agents/skills/odoo-19/references/odoo-19-translation-guide.md b/.agents/skills/odoo-19/references/odoo-19-translation-guide.md new file mode 100644 index 00000000..eb551e64 --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-translation-guide.md @@ -0,0 +1,328 @@ +# Odoo 19 Translation Guide + +Guide for adding translations and localization in Odoo 19: Python, JavaScript, and QWeb templates. + +## Table of Contents + +- [Translation Overview](#translation-overview) +- [Python Translations](#python-translations) +- [JavaScript Translations](#javascript-translations) +- [QWeb Translations](#qweb-translations) +- [Translated Fields](#translated-fields) +- [Export/Import](#exportimport) +- [Languages](#languages) + +--- + +## Translation Overview + +Odoo supports multi-language through: + +- Python `_()` function +- JavaScript `_t()` function +- Translatable fields +- QWeb translation mechanisms + +### Supported File Formats + +| Format | Use | +| ------ | ----------------------------- | +| `.po` | Portable Object (main format) | +| `.pot` | Portable Object Template | +| `.csv` | For some data imports | + +--- + +## Python Translations + +### Basic Translation + +```python +from odoo import _ + +def my_method(self): + message = _("Hello World") + return message +``` + +### Translation with Parameters + +```python +# Old style (still works) +message = _("Hello %s") % name + +# New style (recommended) +message = _("Hello %(name)s") % {'name': name} +``` + +### Lazy Translation + +```python +from odoo import _ + +# Lazy translation (evaluated when displayed, not when imported) +ERROR_MESSAGE = _("Error occurred") + +def my_method(self): + # ERROR_MESSAGE is translated when needed + return {'error': ERROR_MESSAGE} +``` + +### Multi-Line Translation + +```python +message = _( + "This is a long message " + "that spans multiple lines" +) +``` + +### Context Translation + +```python +# Provide context for translators +message = _("Cancel", default_code="refund_cancel") +``` + +--- + +## JavaScript Translations + +### Basic Translation + +```javascript +import { _t } from "@web/core/l10n/translation"; + +const message = _t("Hello World"); +``` + +### Translation with Parameters + +```javascript +const message = _t("Hello %(name)s", { name: "John" }); +``` + +### Lazy Translation + +```javascript +import { lazyTranslation } from "@web/core/l10n/translation"; + +const lt = lazyTranslation(() => _t("Error occurred")); +``` + +### Class Translation + +```javascript +import { _lt } from "@web/core/l10n/translation"; + +class MyClass { + errorMessage = _lt("Error occurred"); +} +``` + +--- + +## QWeb Translations + +### Translate Static Text + +```xml + +``` + +### Translate in Code + +```xml + +``` + +### Translate Field Content + +```xml + +``` + +### Translate Template Content + +```xml + +``` + +--- + +## Translated Fields + +### Define Translated Field + +```python +name = fields.Char(translate=True) +description = fields.Text(translate=True) +``` + +### Translation Options + +```python +# Enable translation +name = fields.Char(translate=True) + +# Translation with context (Odoo 19+) +notes = fields.Text(translate=True, translation_modifiable=True) +``` + +### Read Translated Field + +```python +# Record is fetched in user's language +record = self.env['my.model'].browse(record_id) +print(record.name) # Translated name +``` + +### Read in Specific Language + +```python +# Read in French +record_fr = record.with_context(lang='fr_FR') +print(record_fr.name) # French name +``` + +--- + +## Export/Import + +### Export Translations + +**Via CLI**: + +```bash +odoo-bin -d mydb -l fr --i18n-export=fr --stop-after-init +``` + +**Via UI**: + +1. Settings → Translations → Export Translations +2. Select language +3. Choose file format (PO) +4. Export + +### Import Translations + +**Via CLI**: + +```bash +odoo-bin -d mydb -l fr --i18n-import=/path/to/fr.po --stop-after-init +``` + +**Via UI**: + +1. Settings → Translations → Import Translations +2. Select language +3. Upload PO file +4. Import + +### Update Translations + +**Via CLI**: + +```bash +odoo-bin -d mydb -l fr --i18n-overwrite --stop-after-init +``` + +--- + +## Languages + +### Install Language + +```python +# Load language +language = self.env['res.lang'].load_lang(self.env.cr, self._uid, 'fr_FR') +``` + +### Available Languages + +```python +languages = self.env['res.lang'].search([]) +for lang in languages: + print(lang.code, lang.name) +``` + +### Get User Language + +```python +user_lang = self.env.user.lang +context_lang = self.env.context.get('lang', 'en_US') +``` + +--- + +## Translation Best Practices + +### Always Use Translation Functions + +```python +# GOOD +message = _("Hello World") + +# BAD +message = "Hello World" +``` + +### Use Parameters for Dynamic Content + +```python +# GOOD +message = _("Hello %(name)s") % {'name': name} + +# BAD +message = _("Hello ") + name +``` + +### Provide Context When Needed + +```python +# GOOD (with context) +message = _("Cancel", default_code="refund_cancel") + +# BAD (ambiguous) +message = _("Cancel") +``` + +### Don't Concatenate Translations + +```python +# BAD +message = _("Hello ") + name + _("!") + +# GOOD +message = _("Hello %(name)s!") % {'name': name} +``` + +--- + +## Common Translation Terms + +| English | French | German | Spanish | +| ------- | ----------- | ---------- | -------- | +| Save | Enregistrer | Speichern | Guardar | +| Cancel | Annuler | Abbrechen | Cancelar | +| Delete | Supprimer | Löschen | Eliminar | +| Edit | Modifier | Bearbeiten | Editar | +| Create | Créer | Erstellen | Crear | +| Search | Rechercher | Suchen | Buscar | + +--- + +## References + +- Odoo 19 translation documentation +- GNU gettext documentation diff --git a/.agents/skills/odoo-19/references/odoo-19-view-guide.md b/.agents/skills/odoo-19/references/odoo-19-view-guide.md new file mode 100644 index 00000000..9e44aaaa --- /dev/null +++ b/.agents/skills/odoo-19/references/odoo-19-view-guide.md @@ -0,0 +1,454 @@ +# Odoo 19 View Guide + +Guide for creating views in Odoo 19: list, form, search, kanban, calendar, graph, pivot, and QWeb templates. + +## Table of Contents + +- [View Types](#view-types) +- [List Views](#list-views) +- [Form Views](#form-views) +- [Search Views](#search-views) +- [Kanban Views](#kanban-views) +- [Calendar Views](#calendar-views) +- [Graph Views](#graph-views) +- [Pivot Views](#pivot-views) +- [View Inheritance](#view-inheritance) +- [QWeb Templates](#qweb-templates) + +--- + +## View Types + +| Type | Tag | Description | +| -------- | ------------ | ------------------------------ | +| List | `` | Table view (formerly ``) | +| Form | `
` | Single record view | +| Search | `` | Search/filter view | +| Kanban | `` | Card-based view | +| Calendar | `` | Calendar view | +| Graph | `` | Chart view | +| Pivot | `` | Pivot table view | +| Activity | `` | Activity view | +| QWeb | `