Compare commits
309 commits
16.0
...
18.0-ci-fo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87c2d1d1d3 | ||
|
|
ca526d55cc | ||
|
|
9e05a279d7 | ||
|
|
1960f40301 | ||
|
|
fbd43bb459 | ||
|
|
d9d9e8be2d | ||
|
|
fad510f69e | ||
| 76dc355c6c | |||
|
|
5b2202f7d4 | ||
|
|
57bf688169 | ||
|
|
7ae10abda8 | ||
|
|
40c92613d9 | ||
|
|
394f9a60a4 | ||
|
|
512dd9f00d | ||
|
|
e3140817a3 | ||
|
|
a029fd9265 | ||
|
|
96b2e8eedd | ||
|
|
6a59d9f6ad | ||
|
|
ef1283be7c | ||
|
|
704f0def1b | ||
|
|
2e7549f857 | ||
|
|
d433e50f2f | ||
|
|
cb32fb6c0d | ||
|
|
a9d6f52ca6 | ||
|
|
5766406b6f | ||
|
|
e625b0c2f3 | ||
|
|
f81c1ca8e7 | ||
|
|
5a2f5d120f | ||
|
|
817ff31d39 | ||
|
|
3e4bd5e5db | ||
|
|
3eb79e2431 | ||
|
|
b3999e2283 | ||
|
|
6ba554c91b | ||
|
|
a67181ab42 | ||
|
|
f3e39550c2 | ||
|
|
4c1813a811 | ||
|
|
5de7573fba | ||
|
|
162ba0b570 | ||
|
|
158933e96e | ||
|
|
e59c706ef6 | ||
|
|
157b344d4b | ||
|
|
23edee6154 | ||
|
|
2b1cabbb43 | ||
|
|
4615da6dae | ||
|
|
fb5b3d1f7e | ||
|
|
72d3cd5dce | ||
|
|
20311740a2 | ||
|
|
40b9126eee | ||
|
|
aae6b6096e | ||
|
|
0ef0c9951c | ||
|
|
42da5ed3db | ||
|
|
65818b0155 | ||
|
|
f2194c5367 | ||
|
|
73660ee226 | ||
|
|
f14aab003a | ||
|
|
df1a9f589a | ||
|
|
8bf9779d98 | ||
|
|
5a6d9c9894 | ||
| 31d53c428c | |||
| 40d7cc8772 | |||
| c439607ce1 | |||
| ba52c72b78 | |||
|
|
07b0e546c4 | ||
|
|
aba22fd230 | ||
| d8cd83bdaf | |||
| 54965d483f | |||
| c4d32c6add | |||
|
|
80f24bcc46 | ||
|
|
62e0e8a92c | ||
| a754df3914 | |||
|
|
1d6747e703 | ||
|
|
b484e3dc2e | ||
|
|
9484f8c349 | ||
|
|
6e6d1e5256 | ||
|
|
bd8d87060f | ||
|
|
48597ae8c7 | ||
|
|
965c5c2495 | ||
|
|
9a9139711f | ||
|
|
d35c3383b2 | ||
|
|
9a7dfe02db | ||
|
|
8798f321b3 | ||
| a0ba3426c8 | |||
|
|
87dcb4e350 | ||
|
|
a812b4385d | ||
|
|
be7f379c1e | ||
|
|
f5ba2d786c | ||
|
|
483c4ad5a7 | ||
|
|
591dd5f2b2 | ||
|
|
a9a14e7db5 | ||
|
|
c8e0461120 | ||
|
|
3b8cb7582b | ||
|
|
6125515772 | ||
|
|
f5670906f9 | ||
|
|
8b9995526e | ||
|
|
16941b8183 | ||
|
|
72da09715d | ||
|
|
c9188e4f10 | ||
|
|
e2a22c5d71 | ||
|
|
b01794c319 | ||
|
|
98ffcdaefb | ||
|
|
66a5e31060 | ||
|
|
eb515c28e1 | ||
|
|
5eb8274192 | ||
|
|
5eea3e2a23 | ||
|
|
647438e012 | ||
|
|
98fe8676ea | ||
|
|
1b20142b6f | ||
|
|
a6e8c975f2 | ||
|
|
9547193083 | ||
|
|
f25d2b5e2c | ||
|
|
4d5e0c5502 | ||
|
|
b0dc189708 | ||
|
|
e1c3da6246 | ||
|
|
876e7b1fbf | ||
|
|
d3d1231d29 | ||
|
|
4ea0286b52 | ||
|
|
56509892e6 | ||
|
|
07bd632537 | ||
|
|
f983d71ea3 | ||
|
|
27097004fb | ||
|
|
a1899483c5 | ||
|
|
9d614ed79e | ||
|
|
fb1efb0474 | ||
|
|
91301fc271 | ||
|
|
b14a6f84ed | ||
|
|
eb81e7db6a | ||
|
|
a4410b9b9e | ||
|
|
91cfb9e137 | ||
|
|
a997331c2d | ||
|
|
f8ef927a9e | ||
|
|
5bb5d20244 | ||
|
|
8f7eca45b8 | ||
|
|
3ca90578ae | ||
|
|
4a928e92dd | ||
|
|
3372cb453b | ||
|
|
1b20b23fc0 | ||
|
|
1d4971c803 | ||
|
|
828278573d | ||
|
|
b73f031dfb | ||
| 9376d03d9d | |||
| de0520d5a8 | |||
|
|
18fd73ed1b | ||
|
|
05a8908007 | ||
|
|
2237cba034 | ||
|
|
ce393b6034 | ||
|
|
e9809b90e9 | ||
|
|
135967019e | ||
|
|
0eb7957a70 | ||
|
|
26bfa028d1 | ||
|
|
39b9f887a9 | ||
|
|
5975e4713a | ||
|
|
6a748ca308 | ||
|
|
7d11a95344 | ||
|
|
331a2e8944 | ||
|
|
813c8071d9 | ||
|
|
ff87243476 | ||
|
|
c17bd7eddd | ||
|
|
603e6b6aa1 | ||
|
|
2caf7673d7 | ||
|
|
496e8918cb | ||
|
|
d4be0ae23e | ||
|
|
12d434d4c7 | ||
|
|
7fc42625a3 | ||
|
|
cd68e8bb5b | ||
|
|
ac00294623 | ||
|
|
7e13ffef07 | ||
|
|
6628e17fef | ||
|
|
5dbea009c0 | ||
|
|
cfde009b64 | ||
|
|
439b7a6fda | ||
|
|
4421e93bda | ||
|
|
0aaadd5fc2 | ||
|
|
c345699bf4 | ||
|
|
89c008441e | ||
|
|
5efe57dc19 | ||
|
|
e2ced75ecd | ||
|
|
c3173a32c9 | ||
|
|
80539f3e36 | ||
|
|
4141fc5ab1 | ||
|
|
d3f26120b0 | ||
|
|
3eeca66551 | ||
|
|
ad8b759643 | ||
|
|
9c14e1dc1a | ||
|
|
eaedf0b421 | ||
|
|
33c148e6a1 | ||
|
|
9bd48654fd | ||
|
|
62a1c4e1e9 | ||
|
|
6cc0a18de3 | ||
|
|
a9c1f1f609 | ||
|
|
aef57a3de4 | ||
|
|
d294843241 | ||
|
|
55497327e8 | ||
|
|
ced21cc489 | ||
|
|
97dc41d212 | ||
|
|
2f57a5d14e | ||
|
|
6935d8fc83 | ||
|
|
6f593c6240 | ||
|
|
a483925005 | ||
|
|
6381a2d985 | ||
|
|
d58c621ef1 | ||
|
|
130a5ff6c4 | ||
|
|
4a668b3240 | ||
|
|
67215f7684 | ||
|
|
9937e987f4 | ||
|
|
c2f9f347b7 | ||
|
|
2a005a9d5a | ||
|
|
539cd5cccd | ||
|
|
3f822a28cf | ||
|
|
1779c42e9b | ||
|
|
e4e03d4794 | ||
|
|
c8b83cc333 | ||
|
|
1a8f92a01e | ||
|
|
c367e20fc5 | ||
|
|
464ca48127 | ||
|
|
92b35ccd6d | ||
|
|
7b343ef198 | ||
|
|
c1226e720b | ||
|
|
d90f2cdc61 | ||
|
|
75ebb7b907 | ||
|
|
4a4639f13a | ||
|
|
b31df7b9d8 | ||
|
|
449bb75bb6 | ||
|
|
07cc0eb517 | ||
|
|
55406ca22d | ||
|
|
0f239601ce | ||
|
|
cafa19ffea | ||
|
|
32f345bc44 | ||
|
|
f35bf0c5a1 | ||
|
|
ed8c6acd92 | ||
|
|
cf9ea887c1 | ||
|
|
380d05785f | ||
|
|
0a2cc4c8c4 | ||
|
|
b8f55135d9 | ||
|
|
a1431d3521 | ||
|
|
02a4758635 | ||
|
|
625b9582b3 | ||
|
|
5d4552581c | ||
|
|
19eb1b91b5 | ||
|
|
fee8ec9c45 | ||
|
|
ed048c85eb | ||
|
|
dbf5bd38b4 | ||
|
|
6fbc7b9456 | ||
|
|
5c89795e30 | ||
|
|
8b0a402ccf | ||
|
|
c70de71cff | ||
|
|
267059fa1b | ||
|
|
b07b7dc671 | ||
|
|
b15e9bc977 | ||
|
|
dc44ace78f | ||
|
|
40ce973bd6 | ||
|
|
5eb039ffe0 | ||
|
|
534876242e | ||
|
|
40db038e15 | ||
|
|
4c1b18ec30 | ||
|
|
fbcc1dfaa2 | ||
|
|
f2a8596d75 | ||
|
|
5721687488 | ||
|
|
e29d7e41d4 | ||
|
|
e59df5a428 | ||
|
|
8e5a4a39e0 | ||
|
|
83b6cca09a | ||
|
|
6fed8639ed | ||
|
|
0a0cf5a018 | ||
|
|
df572337d6 | ||
|
|
9000e92324 | ||
|
|
eb6b53db1a | ||
|
|
9807feef90 | ||
|
|
8b728b8b7c | ||
|
|
23e156a13e | ||
|
|
a128c1ee1e | ||
|
|
1f37f289ba | ||
|
|
10ae5bcbf6 | ||
|
|
d90f043617 | ||
|
|
a1317b8ade | ||
|
|
5ba8ddda92 | ||
|
|
0d5f0be88c | ||
|
|
115c9c0cc4 | ||
|
|
713acd065e | ||
|
|
b5410d24bc | ||
|
|
3eae4fa884 | ||
|
|
4bb7edfbec | ||
|
|
1208990be3 | ||
|
|
2eaef82f3a | ||
|
|
6d94484710 | ||
|
|
f3a258766b | ||
|
|
b916779a67 | ||
|
|
55811d54b1 | ||
|
|
fd83d31188 | ||
|
|
4b78dc4447 | ||
|
|
70ed972e23 | ||
|
|
c308d538a3 | ||
|
|
f5a689bcc8 | ||
|
|
4207afbc3f | ||
|
|
0889cdfc3a | ||
|
|
b10ba1fc15 | ||
|
|
2a480b74bb | ||
|
|
4d23e98f7b | ||
|
|
3cb0af6a7b | ||
|
|
ec9f5a572c | ||
|
|
fe137dc265 | ||
|
|
5b9c6e3211 | ||
|
|
370c8ca66a | ||
|
|
7cff89e418 | ||
|
|
69917d1ec2 | ||
|
|
39eb9608dc | ||
|
|
80c2617c40 | ||
|
|
e27cacd65b | ||
|
|
1bcc31b810 | ||
|
|
123aabb775 |
1071 changed files with 71388 additions and 18091 deletions
149
.claude/commands/opsx/apply.md
Normal file
149
.claude/commands/opsx/apply.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
description: Implement tasks from an OpenSpec change (Experimental)
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! You can archive this change with `/opsx:archive`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
154
.claude/commands/opsx/archive.md
Normal file
154
.claude/commands/opsx/archive.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
description: Archive a completed change in the experimental workflow
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / sync skipped / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
```
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** Sync skipped (user chose to skip)
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta spec sync was skipped (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
```
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
```
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
170
.claude/commands/opsx/explore.md
Normal file
170
.claude/commands/opsx/explore.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||
- A vague idea: "real-time collaboration"
|
||||
- A specific problem: "the auth system is getting unwieldy"
|
||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||
- A comparison: "postgres vs sqlite for this"
|
||||
- Nothing (just enter explore mode)
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
If the user mentioned a specific change name, read its artifacts for context.
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
103
.claude/commands/opsx/propose.md
Normal file
103
.claude/commands/opsx/propose.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
description: Propose a new change - create it and generate all artifacts in one step
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
24
.claude/skills/odoo-javascript/SKILL.md
Normal file
24
.claude/skills/odoo-javascript/SKILL.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: odoo-javascript
|
||||
description: JavaScript/Odoo frontend conventions for this repo (modules, tours, events, lazy loading). Use when editing JS in static/src/js/ of Odoo addons.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# JavaScript (Odoo Web/Frontend)
|
||||
|
||||
## Style and conventions
|
||||
- ES6+ (`let`/`const`, arrow functions). Linting with ESLint (`.eslintrc.js` at root).
|
||||
- Odoo pattern: AMD modules (`odoo.define`).
|
||||
- **No business logic in JS (pricing rules, stock/availability decisions, permission checks)** — these belong in Python. JS handles only rendering, DOM events, and calling Python endpoints via `ajax.jsonRpc`.
|
||||
- Comments to explain hacks or workarounds.
|
||||
|
||||
## Key patterns
|
||||
- Scripts in `static/src/js/`.
|
||||
- UI tours registered with `web_tour.tour` (test: true, url, steps).
|
||||
- **Delegated** events for dynamic elements (lazy loading, infinite scroll in `website_sale_aplicoop`).
|
||||
- AJAX via Odoo helpers (`ajax.jsonRpc`).
|
||||
|
||||
## Common mistakes
|
||||
- Business logic in JS → move it to Python.
|
||||
- Listeners lost after DOM manipulation → reattach via event delegation.
|
||||
37
.claude/skills/odoo-python/SKILL.md
Normal file
37
.claude/skills/odoo-python/SKILL.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
name: odoo-python
|
||||
description: Python/Odoo ORM conventions for this repo (models, fields, computed, tests, OCA). Use when writing or modifying Python code in Odoo addons (.py, models/, wizards/, tests/).
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Python (Odoo ORM)
|
||||
|
||||
## Style and conventions
|
||||
- Follow the **OCA guidelines** strictly.
|
||||
- `black` (line length 88) for formatting, `isort` (black profile) for imports.
|
||||
- Mandatory linting: `flake8` and `pylint-odoo`.
|
||||
- Docstrings on classes and public methods; inline comments only for complex logic.
|
||||
- **Do NOT use `_()` in field definitions** (import-time warning). Only in executable methods.
|
||||
|
||||
## Key patterns
|
||||
- Models inherit from `models.Model` or `models.TransientModel` (wizards).
|
||||
- **Business fields ALWAYS on `product.product`**, not on `product.template` (use `related` on the template).
|
||||
Avoids issues with pricelists and reports that operate at the variant level.
|
||||
- `@api.depends(...)` for computed fields.
|
||||
- Notifications: `ir.actions.client` with `display_notification` (`type`: success/warning/danger/info).
|
||||
- Detailed price/discount logging: `_logger.info("[PRICE] Product %s [%s]: ...", code, id, ...)`.
|
||||
- Bulk updates: prefer `search().write()` over loops; `create([vals, ...])` for bulk create.
|
||||
- Pricing: use `product_get_price_helper` (`product._get_price(qty=1, pricelist=pricelist)`).
|
||||
Validate `product.taxes_id` before computing (raise `UserError` if missing).
|
||||
|
||||
## Testing
|
||||
- Tests in each addon's `tests/`, named `test_*.py`, inheriting from `odoo.tests.common.TransactionCase`.
|
||||
- Run (use `run`, not `exec`, for a clean container):
|
||||
```bash
|
||||
docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u <addon>
|
||||
```
|
||||
|
||||
## Common mistakes
|
||||
- `_()` in module-level field definitions → warning. Remove it; translation goes through `.po`.
|
||||
- Computed fields failing in pricelists/reports → move logic to `product.product`.
|
||||
35
.claude/skills/odoo-qweb-html/SKILL.md
Normal file
35
.claude/skills/odoo-qweb-html/SKILL.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: odoo-qweb-html
|
||||
description: HTML/CSS/QWeb conventions for this repo (web templates, moving logic to the controller, styling). Use when editing QWeb templates, web views, or CSS in Odoo addons (website_sale_aplicoop).
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# HTML & CSS (Odoo/QWeb)
|
||||
|
||||
## Golden rule
|
||||
**QWeb has strict limitations with complex logic.** Prepare ALL data in the Python controller
|
||||
and use only simple attribute access in the template. This avoids errors like
|
||||
`TypeError: 'NoneType' object is not callable`.
|
||||
|
||||
```python
|
||||
# ❌ BAD — QWeb does not reliably parse this
|
||||
# <t t-set="price" t-value="info.get('price') or product.list_price or 0"/>
|
||||
|
||||
# ✅ GOOD — pre-process in the controller
|
||||
def _prepare_product_display_info(self, product, price_info):
|
||||
price = price_info.get(product.id, {}).get('price') or product.list_price or 0.0
|
||||
return {'display_price': float(price), 'safe_uom': product.uom_id.category_id.name or ''}
|
||||
```
|
||||
```xml
|
||||
<!-- In template: simple access, no conditionals -->
|
||||
<span t-esc="product_display['display_price']"/>
|
||||
```
|
||||
|
||||
## Style and conventions
|
||||
- Valid HTML5. 4-space indentation.
|
||||
- CSS: BEM or utility classes / Bootstrap when available. **No inline styles** except in well-justified cases.
|
||||
- Templates in `views/*.xml` or `static/src/xml/`. Styles in `static/src/css/` (load in manifest if needed).
|
||||
|
||||
## Reference
|
||||
- `docs/QWEB_BEST_PRACTICES.md` for full logic/template separation patterns.
|
||||
182
.claude/skills/odoo-web-design/SKILL.md
Normal file
182
.claude/skills/odoo-web-design/SKILL.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
---
|
||||
name: odoo-web-design
|
||||
description: Professional finish for the Odoo frontend of this repo — visual polish, responsive layout and WCAG accessibility across CSS, QWeb/HTML and JS. Use when styling or reviewing website/portal pages (website_sale_aplicoop), adding UI components, or fixing responsive/a11y/design-consistency issues.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Web design & finish (Odoo 18 frontend)
|
||||
|
||||
Covers the three layers together: `static/src/css/`, `static/src/js/`, and the QWeb templates in
|
||||
`views/*.xml`. For language-level conventions see `odoo-qweb-html`, `odoo-javascript`,
|
||||
`odoo-xml-views`; this skill is about the **finish**: does it look designed, does it work at 320 px,
|
||||
does it work without a mouse.
|
||||
|
||||
## Inherited non-negotiables
|
||||
|
||||
- No logic in QWeb — prepare everything in the controller.
|
||||
- No business logic in JS — UI and events only.
|
||||
- **No inline `style="..."`** in templates. A style needs a class, and the class needs a token.
|
||||
- Every user-visible string is translatable (see §4).
|
||||
|
||||
## 1. Work order
|
||||
|
||||
Never start by editing a component. Always:
|
||||
|
||||
1. **Audit** with the commands in §7. Write down what is inconsistent before touching anything.
|
||||
2. **Tokens first** (`base/variables.css`). Every value a component needs must already exist as a token.
|
||||
3. **Component by component**, one file each, media queries co-located at the bottom of that file.
|
||||
4. **Accessibility pass** per template, top to bottom.
|
||||
5. **Verify** — lint, contrast, keyboard-only, 320 px, 200 % zoom.
|
||||
|
||||
Doing 3 before 2 is how a codebase ends up with 61 `!important` and three shades of the same blue.
|
||||
|
||||
## 2. Design tokens
|
||||
|
||||
Odoo 18 ships Bootstrap 5.3 with `$variable-prefix: ''`
|
||||
(`/media/snt/data/odoo/ocb/addons/web/static/src/scss/bootstrap_overridden.scss:51`), so Bootstrap's
|
||||
root custom properties are emitted **unprefixed** into `web.assets_frontend` (via
|
||||
`import_bootstrap.scss` → `@import "root"`). Available in plain CSS, no SCSS needed:
|
||||
|
||||
```
|
||||
--primary --secondary --success --danger --warning --info --light --dark
|
||||
--body-color --body-bg --emphasis-color --secondary-bg --tertiary-bg --border-color
|
||||
--border-radius --border-radius-sm --border-radius-lg --border-radius-xl --border-radius-pill
|
||||
--link-color --link-hover-color --focus-ring-color --focus-ring-width
|
||||
--box-shadow --box-shadow-sm --box-shadow-lg --font-sans-serif --gray-100 … --gray-900
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- **Alias, don't copy.** `--ac-color-primary: var(--primary, #007bff);` keeps the website theme
|
||||
editor in control of the palette. A hardcoded brand hex breaks theming silently.
|
||||
- **Hex literals live only in `base/variables.css`.** A hex anywhere else is a defect.
|
||||
- **Prefix new tokens** (`--ac-…`, aplicoop). Bare names like `--primary-color` sit in Bootstrap's
|
||||
unprefixed namespace and will collide as Odoo adds tokens.
|
||||
- **Never redefine a token inside a media query** to shrink it — use `clamp()` so it scales
|
||||
continuously instead of jumping at one width.
|
||||
- Space and radius come from the scale, never from a typed-in number. If you need a value the scale
|
||||
doesn't have, the scale is wrong — fix the scale.
|
||||
|
||||
## 3. Responsive
|
||||
|
||||
- **Mobile-first, `min-width` only.** Bootstrap 5 breakpoints exclusively: **576 / 768 / 992 / 1200 /
|
||||
1400**. No 480/720/1600 one-offs — a private breakpoint is a design decision nobody else knows about.
|
||||
- **One range per rule.** Pairing `min-width` and `max-width` to carve a band is almost always a
|
||||
symptom of desktop-first thinking leaking in.
|
||||
- **Media queries belong with their component**, at the bottom of its file. A central
|
||||
`responsive.css` drifts out of sync with the components it overrides and forces `!important`.
|
||||
Don't add to it; empty it as you touch each component.
|
||||
If a rule in `responsive.css` spans multiple components or has no clear owner, move it to the
|
||||
relevant layout partial (e.g. `layout/grid.css`) rather than duplicating it per component; note
|
||||
any such moves in the audit output.
|
||||
- **Prefer intrinsic layout to breakpoints**: `repeat(auto-fill, minmax(min(100%, 16rem), 1fr))`,
|
||||
`clamp()`, `flex-wrap`. Most grids need zero media queries.
|
||||
- `rem` for everything; `px` only for hairlines, borders and shadow offsets.
|
||||
- Touch targets: **24×24 px is the AA floor** (WCAG 2.2 §2.5.8); **44×44 px is §2.5.5, level AAA**,
|
||||
and the size a thumb actually needs — apply it under `@media (pointer: coarse)`. Quantity steppers
|
||||
and icon buttons are the usual offenders.
|
||||
- Must survive **320 px wide** and **200 % zoom** with no horizontal scroll.
|
||||
|
||||
Details and recipes: `references/responsive.md`.
|
||||
|
||||
## 4. Accessibility — WCAG 2.2 AA baseline
|
||||
|
||||
- **Semantics before ARIA**: `<button>`, `<nav>`, `<ul>`, `<label for>`. ARIA only when no element
|
||||
expresses the meaning. A `<div onclick>` is never acceptable.
|
||||
- **Contrast**: 4.5:1 body text, 3:1 for ≥24 px (or bold ≥19 px), 3:1 for form-control borders,
|
||||
icon-only buttons and focus rings.
|
||||
- **Focus**: never `outline: none` without a replacement. Style `:focus-visible`, not `:focus` — the
|
||||
latter also fires on mouse click and gets removed out of frustration.
|
||||
- Every input has a real `<label>`. A placeholder is not a label.
|
||||
- Icon-only control → translatable `aria-label`, or `.visually-hidden` text (Bootstrap ships the class).
|
||||
- Dynamic updates (cart total, quantity, result count) → `aria-live="polite"` region.
|
||||
- Images: meaningful `alt`, or `alt=""` when decorative. Never omit the attribute.
|
||||
- Status is never conveyed by colour alone — pair it with text or an icon.
|
||||
- Every animation you add needs a `@media (prefers-reduced-motion: reduce)` escape.
|
||||
|
||||
**Translatable QWeb attributes** (auto-extracted, `odoo/tools/translate.py:163`), plus their
|
||||
`t-attf-` forms:
|
||||
|
||||
```
|
||||
string help placeholder alt title label data-tooltip confirm
|
||||
aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext
|
||||
```
|
||||
|
||||
`aria-description` is **not** translated — use `aria-label` or visually-hidden text instead.
|
||||
Strings injected from JS go through `window.i18nManager.get(key)` (served by `/eskaera/i18n`), with
|
||||
the key added server-side — never a hardcoded literal, including in live-region announcements.
|
||||
|
||||
Per-component patterns: `references/accessibility.md`.
|
||||
|
||||
## 5. JS for finish (UI only)
|
||||
|
||||
- **Delegated events** — content is lazy-loaded and infinite-scrolled, direct listeners die.
|
||||
- After injecting nodes: move or restore focus deliberately, announce the new count in the live
|
||||
region, and set `aria-busy` on the container while fetching.
|
||||
- Check `matchMedia("(prefers-reduced-motion: reduce)")` before animating or smooth-scrolling.
|
||||
- Toggle **classes**, don't write `element.style.*` — inline styles from JS are the same defect as
|
||||
inline styles in templates, just later.
|
||||
- Debounce search input ≥ 200 ms and announce "N results" politely.
|
||||
- Batch DOM reads and writes; never read `offsetHeight` inside a loop that also writes.
|
||||
|
||||
## 6. Asset delivery
|
||||
|
||||
`website_sale.css` pulls its 17 partials with `@import`. Odoo rewrites those to absolute URLs and
|
||||
hoists them to the top of the bundle (`odoo/addons/base/models/assetsbundle.py:909` and `:525`), so
|
||||
they ship as ~17 extra render-blocking requests **outside** the minified bundle. For a production
|
||||
finish, prefer listing each file in the manifest's `web.assets_frontend` (order: variables →
|
||||
utilities → layout → components → sections). Only rename them to `.scss` if the maintainer
|
||||
explicitly requests SCSS compilation so Odoo inlines the imports at compile time.
|
||||
|
||||
## 7. Audit & verify
|
||||
|
||||
If `/media/snt/data/addons-cm/website_sale_aplicoop` does not exist, run the audit commands from the
|
||||
repository root of the current workspace instead, and report which path was used.
|
||||
|
||||
```bash
|
||||
cd /media/snt/data/addons-cm/website_sale_aplicoop
|
||||
|
||||
# Breakpoint sprawl — should only ever list 576/768/992/1200/1400, all min-width
|
||||
grep -rho "@media[^{]*" static/src/css | sort | uniq -c | sort -rn
|
||||
|
||||
# Hardcoded colours outside the token file (expect: none)
|
||||
grep -rn "#[0-9a-fA-F]\{3,8\}\b" static/src/css --include="*.css" | grep -v "base/variables.css"
|
||||
|
||||
# Specificity debt
|
||||
grep -rc "!important" static/src/css/*/*.css | grep -v ':0'
|
||||
|
||||
# Focus handling: :focus-visible should dominate, outline:none must be paired with a replacement
|
||||
grep -rn ":focus-visible\|outline: *none\|outline: *0" static/src/css
|
||||
|
||||
# Motion safety (expect at least one per animating component)
|
||||
grep -rn "prefers-reduced-motion" static/src/css
|
||||
|
||||
# Inline styles in templates (expect: none)
|
||||
grep -rn 'style="' views/*.xml
|
||||
|
||||
# Images without alt
|
||||
grep -o "<img[^>]*" views/*.xml | grep -v "alt="
|
||||
|
||||
# Interactive divs/spans (expect: none)
|
||||
grep -rn "<div[^>]*t-on-click\|<span[^>]*t-on-click\|<div[^>]*onclick" views/*.xml
|
||||
```
|
||||
|
||||
Then, always:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files # prettier (width 100, indent 4) over css/js/xml + eslint
|
||||
docker-compose up -d # http://localhost:8070 — check at 320/768/1280 and 200% zoom
|
||||
```
|
||||
|
||||
Manual passes that no grep replaces: **Tab through the whole page** (visible focus everywhere, order
|
||||
matches visual order, no trap), and check contrast on the actual rendered colours.
|
||||
|
||||
## 8. References
|
||||
|
||||
- `references/accessibility.md` — WCAG AA patterns as QWeb snippets, per component.
|
||||
- `references/responsive.md` — breakpoints, intrinsic layout, images, test matrix.
|
||||
- `references/css-architecture.md` — file layout, naming, specificity, killing `!important`.
|
||||
- `references/finish-checklist.md` — the polish pass: states, rhythm, empty/loading/error.
|
||||
|
||||
Repo docs: `docs/QWEB_BEST_PRACTICES.md`, `docs/LAZY_LOADING.md`.
|
||||
245
.claude/skills/odoo-web-design/references/accessibility.md
Normal file
245
.claude/skills/odoo-web-design/references/accessibility.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# Accessibility patterns (QWeb + CSS + JS)
|
||||
|
||||
WCAG 2.2 AA. Snippets use this repo's conventions: 4-space indent, Bootstrap 5.3 utilities,
|
||||
translatable attributes, `window.i18nManager.get(key)` for JS strings.
|
||||
|
||||
## Page skeleton
|
||||
|
||||
```xml
|
||||
<t t-call="website.layout">
|
||||
<a href="#eskaera-main" class="visually-hidden-focusable btn btn-primary m-2">
|
||||
Skip to main content
|
||||
</a>
|
||||
<div id="wrap" class="eskaera-shop-page">
|
||||
<main id="eskaera-main" tabindex="-1">
|
||||
<h1 class="eskaera-page-title" t-esc="page_title"/>
|
||||
...
|
||||
</main>
|
||||
</div>
|
||||
</t>
|
||||
```
|
||||
|
||||
- Exactly one `<h1>` per page; never skip heading levels to get a font size — that's what classes are for.
|
||||
- `<main>`, `<nav aria-label="...">`, `<aside>` instead of anonymous `<div>`s.
|
||||
- `tabindex="-1"` on the skip-link target so focus actually lands there.
|
||||
|
||||
## Buttons vs links
|
||||
|
||||
| Intent | Element |
|
||||
| --- | --- |
|
||||
| Navigates to a URL | `<a href="...">` |
|
||||
| Changes state on this page | `<button type="button">` |
|
||||
| Submits a form | `<button type="submit">` |
|
||||
|
||||
Never `<div t-on-click>` / `<span onclick>` — no keyboard, no role, no focus.
|
||||
|
||||
Icon-only:
|
||||
|
||||
```xml
|
||||
<button type="button" class="btn btn-outline-secondary qty-decrease"
|
||||
aria-label="Decrease quantity">
|
||||
<i class="fa fa-minus" aria-hidden="true"/>
|
||||
</button>
|
||||
```
|
||||
|
||||
`aria-label` is auto-translated; `aria-hidden` on the icon stops the screen reader announcing
|
||||
"minus icon" twice. Alternative when the label is long: visible `<span class="visually-hidden">`.
|
||||
|
||||
## Quantity stepper (live-announced)
|
||||
|
||||
```xml
|
||||
<div class="qty-control" role="group" aria-label="Quantity">
|
||||
<button type="button" class="btn qty-decrease" aria-label="Decrease quantity">
|
||||
<i class="fa fa-minus" aria-hidden="true"/>
|
||||
</button>
|
||||
<input type="number" class="form-control qty-input"
|
||||
t-att-value="line.qty" min="0" step="1"
|
||||
aria-label="Quantity" inputmode="numeric"/>
|
||||
<button type="button" class="btn qty-increase" aria-label="Increase quantity">
|
||||
<i class="fa fa-plus" aria-hidden="true"/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="visually-hidden" aria-live="polite" id="qty_status"/>
|
||||
```
|
||||
|
||||
```js
|
||||
// after the server confirms the new qty
|
||||
const status = document.getElementById("qty_status");
|
||||
status.textContent = window.i18nManager.get("qty_updated_announcement");
|
||||
```
|
||||
|
||||
Buttons must stay ≥ 24×24 px (WCAG 2.2 AA §2.5.8) everywhere, and ≥ 44×44 px under
|
||||
`@media (pointer: coarse)` — including at 320 px width.
|
||||
|
||||
## Forms and errors
|
||||
|
||||
```xml
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="delivery_note">Delivery note</label>
|
||||
<textarea id="delivery_note" name="delivery_note" class="form-control"
|
||||
t-att-aria-invalid="'true' if errors.get('delivery_note') else None"
|
||||
t-att-aria-describedby="'delivery_note_help delivery_note_error'
|
||||
if errors.get('delivery_note')
|
||||
else 'delivery_note_help'"/>
|
||||
<small id="delivery_note_help" class="form-text">Optional, visible to the group manager.</small>
|
||||
<p t-if="errors.get('delivery_note')" id="delivery_note_error"
|
||||
class="invalid-feedback d-block" t-esc="errors['delivery_note']"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- The `for`/`id` pair is mandatory; wrapping alone is not enough for every AT.
|
||||
- Error text is referenced by `aria-describedby`, and never colour-only — keep the message.
|
||||
- The error summary at the top of a failed form should be focused on submit
|
||||
(`container.focus()` on a `tabindex="-1"` heading).
|
||||
- Required fields: `required` + a visible "(required)" or `*` with a legend explaining it.
|
||||
|
||||
## Product card — one link, no duplicates
|
||||
|
||||
```xml
|
||||
<article class="product-card">
|
||||
<img class="product-card-img" t-att-src="product['image_url']"
|
||||
t-att-alt="product['name']" loading="lazy" width="400" height="400"/>
|
||||
<h3 class="product-card-title">
|
||||
<a t-att-href="product['url']" class="stretched-link" t-esc="product['name']"/>
|
||||
</h3>
|
||||
<p class="product-card-price">
|
||||
<span class="visually-hidden">Price</span>
|
||||
<span t-esc="product['price_formatted']"/>
|
||||
</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
- `stretched-link` makes the whole card clickable with a **single** link in the a11y tree — do not
|
||||
also wrap the image in an `<a>`, that produces two identical stops.
|
||||
- `width`/`height` reserve space (no layout shift); CSS `object-fit: cover` handles the ratio.
|
||||
- Decorative product placeholder → `alt=""`.
|
||||
|
||||
## Filters as toggle buttons
|
||||
|
||||
```xml
|
||||
<div class="tag-filter" role="group" aria-label="Filter by category">
|
||||
<button t-foreach="categories" t-as="cat" t-key="cat['id']"
|
||||
type="button" class="tag-filter-badge"
|
||||
t-att-aria-pressed="'true' if cat['active'] else 'false'"
|
||||
t-att-data-category-id="cat['id']">
|
||||
<span t-esc="cat['name']"/>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
JS flips `aria-pressed` together with the active class — the class alone announces nothing.
|
||||
If the filters are a true multi-select set, `<fieldset><legend>` + checkboxes is better still.
|
||||
|
||||
## Search with results announcement
|
||||
|
||||
```xml
|
||||
<label class="visually-hidden" for="product_search">Search products</label>
|
||||
<input id="product_search" type="search" class="form-control"
|
||||
placeholder="Search products…" autocomplete="off"
|
||||
aria-describedby="search_results_count"/>
|
||||
<p id="search_results_count" class="visually-hidden" aria-live="polite" aria-atomic="true"/>
|
||||
```
|
||||
|
||||
Debounce ≥ 200 ms, then set the text once per settled query — announcing on every keystroke makes
|
||||
the page unusable with a screen reader.
|
||||
|
||||
## Loading, infinite scroll, empty states
|
||||
|
||||
```xml
|
||||
<div id="products_grid" class="products-grid" aria-busy="false">…</div>
|
||||
<p class="visually-hidden" aria-live="polite" id="load_status"/>
|
||||
<button type="button" id="load_more" class="btn btn-outline-primary">Load more products</button>
|
||||
```
|
||||
|
||||
- Set `aria-busy="true"` on the grid while fetching, back to `false` when done.
|
||||
- Announce `"N more products loaded"` via `load_status`; don't move focus mid-scroll.
|
||||
- **Always keep a real "Load more" button** alongside scroll-triggered loading — infinite scroll
|
||||
alone is unreachable by keyboard and traps users away from the footer.
|
||||
- Empty state is content, not a blank div: heading + explanation + the action that fixes it.
|
||||
|
||||
## Modals
|
||||
|
||||
Use Bootstrap's modal (focus trap, Esc, `aria-modal` come free) and give it a label:
|
||||
|
||||
```xml
|
||||
<div class="modal fade" id="confirm_order_modal" tabindex="-1"
|
||||
aria-labelledby="confirm_order_title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="confirm_order_title">Confirm your order</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"/>
|
||||
</div>
|
||||
…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Return focus to the trigger on close (Bootstrap does this if the trigger is a real button).
|
||||
|
||||
## Order tables
|
||||
|
||||
```xml
|
||||
<table class="table checkout-summary-table">
|
||||
<caption class="visually-hidden">Order lines</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Product</th>
|
||||
<th scope="col" class="text-end">Quantity</th>
|
||||
<th scope="col" class="text-end">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="lines" t-as="line" t-key="line['id']">
|
||||
<th scope="row" t-esc="line['name']"/>
|
||||
<td class="text-end" t-esc="line['qty']"/>
|
||||
<td class="text-end" t-esc="line['subtotal_formatted']"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
Never use a table for layout, and never drop `<thead>`/`scope` when stacking to cards on mobile —
|
||||
restyle, don't restructure.
|
||||
|
||||
## Focus and motion in CSS
|
||||
|
||||
```css
|
||||
/* One ring, everywhere, on keyboard focus only. */
|
||||
.eskaera-page :focus-visible {
|
||||
outline: 2px solid var(--ac-color-focus, var(--focus-ring-color, #0d6efd));
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--ac-radius-sm);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the one legitimate `!important` in the codebase — a user preference must beat every component.
|
||||
|
||||
## Multilingual
|
||||
|
||||
The addon runs es / eu / ca. `website.layout` sets `<html lang>`; when a string stays in another
|
||||
language inside a translated page (a supplier name, a product origin), mark it: `<span lang="eu">`.
|
||||
Never build a sentence by concatenating translated fragments — pass the whole string with `%s`
|
||||
placeholders so translators can reorder.
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Keyboard only**: Tab through the page. Visible focus at every stop, order matches the visual
|
||||
order, no trap, every action reachable, Esc closes overlays.
|
||||
2. **Zoom 200 %** and **320 px width**: no horizontal scroll, nothing clipped.
|
||||
3. **Contrast**: check rendered colours, including disabled states and placeholder text.
|
||||
4. **Screen reader** smoke test on the cart flow (Orca on Linux, NVDA on Windows).
|
||||
5. Lighthouse/axe in DevTools catches the mechanical half — it does not catch focus order,
|
||||
announcement quality, or a label that lies.
|
||||
163
.claude/skills/odoo-web-design/references/css-architecture.md
Normal file
163
.claude/skills/odoo-web-design/references/css-architecture.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
|
||||
# CSS architecture
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
static/src/css/
|
||||
├── website_sale.css # index only: @import list, no rules of its own
|
||||
├── base/
|
||||
│ ├── variables.css # :root tokens — the ONLY file with hex literals
|
||||
│ └── utilities.css # tiny single-purpose helpers, when Bootstrap lacks one
|
||||
├── layout/
|
||||
│ ├── pages.css # page shells, containers, backgrounds
|
||||
│ └── header.css
|
||||
├── components/ # one file per reusable component
|
||||
│ ├── product-card.css
|
||||
│ ├── order-card.css
|
||||
│ ├── cart.css
|
||||
│ ├── buttons.css
|
||||
│ ├── quantity-control.css
|
||||
│ ├── forms.css
|
||||
│ ├── alerts.css
|
||||
│ └── tag-filter.css
|
||||
└── sections/ # page-specific composition only
|
||||
├── products-grid.css
|
||||
├── order-list.css
|
||||
├── checkout.css
|
||||
└── info-cards.css
|
||||
```
|
||||
|
||||
Rules of placement:
|
||||
|
||||
- A rule that could appear on two pages is a **component**, never a section.
|
||||
- A **section** file may only position and space components — if it restyles a component's internals,
|
||||
the component is missing a modifier.
|
||||
- Each component file owns its own media queries, at the bottom. There is no global responsive file
|
||||
in a healthy tree; a legacy `layout/responsive.css` should shrink to zero as components are touched.
|
||||
- `website_sale.css` contains imports and comments only.
|
||||
|
||||
## Naming
|
||||
|
||||
Prefix everything with `eskaera-` (the product name already used for page-level classes). Unprefixed
|
||||
names like `.product-card`, `.cart-header`, `.qty-control` sit in the same namespace as Bootstrap,
|
||||
Odoo core and every other installed addon — that collision is what forces `!important` later.
|
||||
|
||||
```
|
||||
.eskaera-product-card /* block */
|
||||
.eskaera-product-card__title /* element */
|
||||
.eskaera-product-card--out-of-stock /* modifier */
|
||||
.is-loading .is-active .has-error /* state, always paired with a block class */
|
||||
```
|
||||
|
||||
`js-` prefix for hooks JS queries, and never style a `js-` class — that way markup can be restyled
|
||||
without breaking behaviour, and behaviour can move without breaking styles.
|
||||
|
||||
When renaming existing classes, do it component by component: template + CSS + JS selectors + tests
|
||||
in the same commit, then grep for the old name across `views/`, `static/src/`, and `README*`.
|
||||
|
||||
## Specificity — how to stop writing `!important`
|
||||
|
||||
Overriding Bootstrap globally (`.list-group-item { … }`, `.alert-warning { … }`) is a fight you win
|
||||
only with `!important`, and it leaks into every other addon. Scope the override under the page root
|
||||
instead: equal specificity plus one class always wins, no escape hatch needed.
|
||||
|
||||
```css
|
||||
/* ❌ global override, needs !important to beat Bootstrap's own later rules */
|
||||
.list-group-item {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* ✅ scoped: higher specificity, no !important, no leakage */
|
||||
.eskaera-checkout-page .list-group-item {
|
||||
border-radius: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Other rules:
|
||||
|
||||
- Max **3** levels of nesting in a selector. Deeper means the markup is doing the work the class
|
||||
should do.
|
||||
- No ID selectors for styling; IDs are for anchors and `aria-*` references.
|
||||
- No element selectors outside `base/` (`div.product-card` locks the markup).
|
||||
- `!important` is allowed in exactly three places, each with a comment saying why: the
|
||||
`prefers-reduced-motion` reset; a genuine **utility** that must beat whatever component it lands on
|
||||
(`.hidden-product`, Bootstrap's own utility layer works this way); and a documented override of a
|
||||
core rule you cannot scope. Anything else means the selector is wrong.
|
||||
|
||||
## Ordering inside a file
|
||||
|
||||
```css
|
||||
.eskaera-product-card {
|
||||
/* 1. layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 2. box */
|
||||
padding: var(--ac-space-md);
|
||||
border: 1px solid var(--ac-border-light);
|
||||
border-radius: var(--ac-radius-lg);
|
||||
/* 3. typography */
|
||||
font-size: var(--ac-text-base);
|
||||
color: var(--ac-text-primary);
|
||||
/* 4. paint & motion */
|
||||
background: var(--ac-surface);
|
||||
box-shadow: var(--ac-shadow-sm);
|
||||
transition: box-shadow var(--ac-transition-fast);
|
||||
}
|
||||
```
|
||||
|
||||
Then, in order: `:hover` (inside `@media (hover: hover)`), `:focus-visible`, `:active`, `:disabled`,
|
||||
state classes, modifiers, media queries.
|
||||
|
||||
## Tokens
|
||||
|
||||
Full rules in `SKILL.md` §2. In short:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* alias Odoo/Bootstrap's unprefixed custom properties so theming still works */
|
||||
--ac-color-primary: var(--primary, #007bff);
|
||||
--ac-color-danger: var(--danger, #dc3545);
|
||||
--ac-surface: var(--body-bg, #fff);
|
||||
--ac-text-primary: var(--body-color, #1a202c);
|
||||
--ac-border-light: var(--border-color, #e2e8f0);
|
||||
|
||||
/* own scales */
|
||||
--ac-space-xs: 0.25rem;
|
||||
--ac-space-sm: 0.5rem;
|
||||
--ac-space-md: 1rem;
|
||||
--ac-space-lg: 1.5rem;
|
||||
--ac-space-xl: 2rem;
|
||||
|
||||
--ac-radius-sm: 0.25rem;
|
||||
--ac-radius-md: 0.5rem;
|
||||
--ac-radius-lg: 0.75rem;
|
||||
|
||||
--ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
--ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
|
||||
--ac-transition-fast: 200ms ease;
|
||||
}
|
||||
```
|
||||
|
||||
Migrating a legacy token set: add the `--ac-` token aliased to the old name, move consumers file by
|
||||
file, delete the old token when `grep -rn "\-\-old-name" static/src/` is empty. Don't rename all at once.
|
||||
|
||||
## Anti-patterns seen in Odoo addons
|
||||
|
||||
| Smell | Fix |
|
||||
| --- | --- |
|
||||
| `!important` to beat Bootstrap | scope under the page root class |
|
||||
| Same colour as `#28a745` in six files | one token in `variables.css` |
|
||||
| `responsive.css` overriding twelve components | move each query into its component |
|
||||
| `style="margin-top:20px"` in QWeb | utility class or component spacing |
|
||||
| `.card .body .row .col span` | give the span a class |
|
||||
| `px` for font sizes | `rem`, so browser zoom and user font size work |
|
||||
| Colour hardcoded instead of `var(--primary)` | breaks the website theme editor silently |
|
||||
| Both `@import` chain **and** manifest entries | double download; pick one (see SKILL.md §6) |
|
||||
|
||||
## Formatting
|
||||
|
||||
Prettier handles `.css` via pre-commit: `printWidth: 100`, `tabWidth: 4`. Run
|
||||
`pre-commit run --all-files` (or `make format`) before committing; never hand-align properties, it
|
||||
will be reformatted.
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# The finish pass
|
||||
|
||||
What separates "it works" from "it looks designed". Go through this per screen, not per file.
|
||||
|
||||
## 1. Every interactive element has all its states
|
||||
|
||||
For each button, link, card, input, filter chip:
|
||||
|
||||
| State | Must be | Common miss |
|
||||
| --- | --- | --- |
|
||||
| default | on the token scale | — |
|
||||
| `:hover` | inside `@media (hover: hover)` | sticks after tap on touch |
|
||||
| `:focus-visible` | visible ring, 3:1 contrast | removed via `outline: none` |
|
||||
| `:active` | perceptible press | missing entirely |
|
||||
| `:disabled` | dimmed **and** `cursor: not-allowed`, still 4.5:1 if it carries text | unreadable grey |
|
||||
| loading | spinner + `aria-busy`, size unchanged | layout jumps |
|
||||
|
||||
The element must not resize between states — animate `box-shadow`, `background`, `transform`, never
|
||||
`padding`, `border-width` or `font-size`.
|
||||
|
||||
## 2. Vertical rhythm and spacing
|
||||
|
||||
- All spacing from `--ac-space-*`. A `margin: 13px` anywhere is a defect.
|
||||
- Space belongs to **one** side consistently (block-end margins, or `gap` — pick one per container).
|
||||
Prefer `gap` in flex/grid; it doesn't collapse and doesn't need `:last-child` cleanups.
|
||||
- Related things sit closer than unrelated things. If the label is as far from its input as from the
|
||||
previous field, the form reads as noise.
|
||||
- Cards in a grid must be equal height (`align-items: stretch` + the price pinned with `margin-top: auto`).
|
||||
|
||||
## 3. Typography
|
||||
|
||||
- One scale (`--ac-text-*`), max 3 weights, max 2 sizes per card.
|
||||
- Headings are structural (`h1`→`h2`→`h3`); size comes from a class, never from picking `h4` because
|
||||
it looks right.
|
||||
- `max-width: 65ch` on paragraphs of real prose.
|
||||
- Numbers in tables: `font-variant-numeric: tabular-nums` and right-aligned so columns line up.
|
||||
- Long product names truncate predictably: `text-overflow: ellipsis` with `title` attribute, or
|
||||
`-webkit-line-clamp: 2` with a fixed `min-height` so cards don't stagger.
|
||||
|
||||
## 4. Colour
|
||||
|
||||
- Semantic, not decorative: success/danger/warning always the same hue for the same meaning.
|
||||
- Status never colour-only — icon or text alongside (colour blindness, and grayscale printing).
|
||||
- Max ~2 accent colours per screen. Everything else is surface, border, text.
|
||||
- Check the rendered contrast, including: placeholder text, disabled buttons, badges on coloured
|
||||
backgrounds, white text on `--warning` (this one almost always fails).
|
||||
|
||||
## 5. The states people forget
|
||||
|
||||
Every list, grid and total needs four designs, not one:
|
||||
|
||||
1. **Loading** — skeletons matched to the real layout (not a centred spinner that collapses the page).
|
||||
2. **Empty** — heading, one sentence explaining why it's empty, and the button that fixes it.
|
||||
"No products found" alone is a dead end; offer "Clear filters".
|
||||
3. **Error** — what failed, in the user's language, and what to do now. Never a raw traceback or code.
|
||||
4. **Partial / stale** — an order past its cutoff, an out-of-stock line, a price that changed. Say it
|
||||
inline, next to the affected row, not only in a toast that disappears.
|
||||
|
||||
## 6. Feedback and motion
|
||||
|
||||
- Any action taken by the user gets a response within 100 ms — disable the button, show the spinner,
|
||||
optimistic-update the quantity. Silence reads as "broken", and users double-submit.
|
||||
- Toasts: `aria-live="polite"`, dismissible, never the only place an error is shown.
|
||||
- Motion is 150–250 ms and eases out. Anything above 400 ms feels slow; anything that moves the page
|
||||
under the cursor is a bug.
|
||||
- Everything animated respects `prefers-reduced-motion`.
|
||||
|
||||
## 7. Content and copy
|
||||
|
||||
- Sentence case for buttons and labels; imperative verbs ("Add to cart", not "Adding products").
|
||||
- Prices always formatted server-side with the currency and locale (`i18nManager.formatCurrency`),
|
||||
never string-concatenated in the template.
|
||||
- Dates: locale-formatted, and relative only when the absolute date is also available (`title`).
|
||||
- Nothing user-visible is a hardcoded English literal — templates get translatable text, JS gets
|
||||
`window.i18nManager.get(key)`.
|
||||
|
||||
## 8. Cross-cutting checks before "done"
|
||||
|
||||
```
|
||||
[ ] 320 px: no horizontal scroll on any page
|
||||
[ ] 200 % zoom: nothing clipped or overlapping
|
||||
[ ] Tab through the whole flow: visible focus, logical order, no trap
|
||||
[ ] All actions reachable without a mouse, Esc closes overlays
|
||||
[ ] No hex outside base/variables.css
|
||||
[ ] No inline style="" in views/*.xml
|
||||
[ ] No new !important (except the reduced-motion reset)
|
||||
[ ] Every <img> has alt and width/height
|
||||
[ ] Every icon-only control has aria-label
|
||||
[ ] Empty / loading / error states exist for each list
|
||||
[ ] es, eu and ca render without overflow (German-length strings break tight buttons)
|
||||
[ ] pre-commit run --all-files is clean
|
||||
[ ] Lighthouse a11y ≥ 95 on shop, cart, checkout
|
||||
```
|
||||
|
||||
The last three matter as much as the rest: a layout tuned only against Spanish copy usually breaks in
|
||||
Basque, and a11y regressions land silently.
|
||||
191
.claude/skills/odoo-web-design/references/responsive.md
Normal file
191
.claude/skills/odoo-web-design/references/responsive.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Responsive layout
|
||||
|
||||
## Breakpoints — Bootstrap 5, and only these
|
||||
|
||||
| Name | `min-width` | Typical device |
|
||||
| --- | --- | --- |
|
||||
| (base) | — | phones, 320–575 px |
|
||||
| `sm` | 576px | large phones |
|
||||
| `md` | 768px | tablets |
|
||||
| `lg` | 992px | small laptops |
|
||||
| `xl` | 1200px | desktops |
|
||||
| `xxl` | 1400px | large desktops |
|
||||
|
||||
Mobile-first, `min-width` only:
|
||||
|
||||
```css
|
||||
/* ✅ base = mobile, enhance upwards */
|
||||
.products-grid {
|
||||
display: grid;
|
||||
gap: var(--ac-space-md);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* ❌ desktop-first bands: overlapping ranges, unclear cascade, private breakpoints */
|
||||
@media (max-width: 1599px) and (min-width: 1400px) { … }
|
||||
@media (max-width: 720px) { … }
|
||||
```
|
||||
|
||||
## Prefer intrinsic layout — most grids need no media query at all
|
||||
|
||||
```css
|
||||
.products-grid {
|
||||
display: grid;
|
||||
gap: var(--ac-space-md);
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
|
||||
}
|
||||
```
|
||||
|
||||
`min(100%, 16rem)` is what keeps it from overflowing at 320 px. Reach for a media query only when the
|
||||
layout must genuinely *change* (sidebar moves, table becomes cards), not merely resize.
|
||||
|
||||
## Fluid type and space
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ac-text-sm: clamp(0.875rem, 0.85rem + 0.1vw, 0.9375rem);
|
||||
--ac-text-base: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
|
||||
--ac-text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
|
||||
--ac-text-xl: clamp(1.5rem, 1.3rem + 1vw, 2.25rem);
|
||||
--ac-space-md: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
|
||||
}
|
||||
```
|
||||
|
||||
One continuous scale beats three step-changes at breakpoints. Never redefine a token inside a media
|
||||
query to make it smaller.
|
||||
|
||||
Body copy: 1rem minimum, line-height ≥ 1.5, measure 45–75 characters (`max-width: 65ch`).
|
||||
|
||||
## Touch targets and pointer
|
||||
|
||||
WCAG 2.2 AA (§2.5.8 Target Size, Minimum) asks for **24×24 px**. The familiar **44×44 px** is §2.5.5,
|
||||
level **AAA** — treat it as the house rule for anything a thumb hits, applied by pointer type rather
|
||||
than by viewport width: a 1024 px tablet needs the big target, a 1024 px laptop doesn't.
|
||||
|
||||
```css
|
||||
.qty-control .btn,
|
||||
.tag-filter-badge {
|
||||
min-width: 2.25rem; /* 36px — comfortable with a mouse */
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.qty-control .btn,
|
||||
.tag-filter-badge {
|
||||
min-width: 2.75rem; /* 44px */
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.product-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--ac-shadow-md);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hover effects belong inside `@media (hover: hover)` — on touch they stick after a tap and look broken.
|
||||
|
||||
## Tables → cards, without restructuring
|
||||
|
||||
```css
|
||||
@media (max-width: 767.98px) {
|
||||
.checkout-summary-table thead {
|
||||
/* keep it in the a11y tree; hide visually only */
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
.checkout-summary-table tr {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--ac-space-xs);
|
||||
padding: var(--ac-space-sm) 0;
|
||||
border-bottom: 1px solid var(--ac-border-light);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the one place a `max-width` query is honest: it's an override of the desktop table. Keep the
|
||||
markup semantic — never emit different HTML per viewport.
|
||||
|
||||
## Sticky bars (cart / checkout actions on mobile)
|
||||
|
||||
```css
|
||||
.checkout-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: var(--ac-z-sticky);
|
||||
padding: var(--ac-space-sm);
|
||||
padding-bottom: calc(var(--ac-space-sm) + env(safe-area-inset-bottom, 0px));
|
||||
background: var(--ac-surface, #fff);
|
||||
box-shadow: 0 -2px 8px rgb(0 0 0 / 8%);
|
||||
}
|
||||
```
|
||||
|
||||
`env(safe-area-inset-bottom)` keeps the button above the iOS home indicator. Sticky elements must not
|
||||
eat more than ~20 % of a 568 px-tall viewport.
|
||||
|
||||
## Images
|
||||
|
||||
```xml
|
||||
<img class="product-card-img" t-att-src="product['image_url']" t-att-alt="product['name']"
|
||||
loading="lazy" decoding="async" width="400" height="400"/>
|
||||
```
|
||||
|
||||
- `width`/`height` always — they reserve the box and prevent layout shift (CLS).
|
||||
- `loading="lazy"` for everything below the fold; **never** on the LCP image (first product row,
|
||||
hero) — that delays the largest paint.
|
||||
- CSS: `max-width: 100%; height: auto;` plus `object-fit: cover` with a fixed `aspect-ratio` when
|
||||
cards must align.
|
||||
- Odoo serves resized variants: `/web/image/product.product/<id>/image_256` etc. Ask for the size you
|
||||
render, not `image_1920` scaled down in CSS.
|
||||
|
||||
## Container and page rhythm
|
||||
|
||||
```css
|
||||
.eskaera-page {
|
||||
width: 100%;
|
||||
max-width: 75rem;
|
||||
margin-inline: auto;
|
||||
padding-inline: clamp(1rem, 4vw, 2rem);
|
||||
}
|
||||
```
|
||||
|
||||
Use logical properties (`margin-inline`, `padding-block`, `inset-inline-start`) — Odoo auto-generates
|
||||
an RTL stylesheet, and logical properties survive the flip without a second rule.
|
||||
|
||||
## Test matrix
|
||||
|
||||
| Width | Why |
|
||||
| --- | --- |
|
||||
| 320 px | smallest supported; the one that breaks first |
|
||||
| 360 / 390 px | real Android / iPhone |
|
||||
| 768 px | tablet portrait, first breakpoint |
|
||||
| 1024 px | tablet landscape / small laptop |
|
||||
| 1280 / 1440 px | desktop |
|
||||
| 1280 px @ 200 % zoom | WCAG 1.4.4 reflow — equivalent to 640 px |
|
||||
|
||||
Run the stack (`docker-compose up -d`, http://localhost:8070) and check, per viewport: no horizontal
|
||||
scroll, no clipped text, touch targets still ≥ 44 px on coarse pointers, sticky bars not covering
|
||||
content, images not stretched.
|
||||
|
||||
DevTools device toolbar is enough for layout; check touch targets by actually tapping on a phone once
|
||||
before calling it done.
|
||||
28
.claude/skills/odoo-xml-views/SKILL.md
Normal file
28
.claude/skills/odoo-xml-views/SKILL.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
name: odoo-xml-views
|
||||
description: XML/Odoo views conventions for this repo (XPath, inheritance, IDs, security, manifest data). Use when editing .xml views, security/, or XML data in Odoo addons.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# XML (Odoo Views)
|
||||
|
||||
## Style and conventions
|
||||
- Indentation: **4 spaces** (no tabs).
|
||||
- XPath with explicit `position`: `before`, `after`, `inside`, `replace`, `attributes`.
|
||||
- Reference groups via `module.xml_id` (e.g. `sales_team.group_sale_manager`).
|
||||
- `sequence` to order fields in views.
|
||||
- IDs: `module.descriptive_name` pattern.
|
||||
- **No conditional logic in QWeb**, only access to attributes prepared in the controller.
|
||||
- Translate strings in XML, never with `_()` in Python field definitions.
|
||||
|
||||
## Key patterns
|
||||
- Views in `views/*.xml`. Typical manifest `data` order:
|
||||
`security/ir.model.access.csv`, `views/actions.xml`, `views/menu.xml`, `views/<model>_views.xml`.
|
||||
- Security: `id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink`.
|
||||
- Common groups: `base.group_user`, `base.group_system`,
|
||||
`sales_team.group_sale_manager`, `purchase.group_purchase_manager`.
|
||||
|
||||
## Common mistakes
|
||||
- *XPath not found* → check inheritance chain and correct XML IDs.
|
||||
- Complex logic in QWeb → move it to the controller (see `docs/QWEB_BEST_PRACTICES.md`).
|
||||
156
.claude/skills/openspec-apply-change/SKILL.md
Normal file
156
.claude/skills/openspec-apply-change/SKILL.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
114
.claude/skills/openspec-archive-change/SKILL.md
Normal file
114
.claude/skills/openspec-archive-change/SKILL.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
name: openspec-archive-change
|
||||
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
288
.claude/skills/openspec-explore/SKILL.md
Normal file
288
.claude/skills/openspec-explore/SKILL.md
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
---
|
||||
name: openspec-explore
|
||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Handling Different Entry Points
|
||||
|
||||
**User brings a vague idea:**
|
||||
```
|
||||
User: I'm thinking about adding real-time collaboration
|
||||
|
||||
You: Real-time collab is a big space. Let me think about this...
|
||||
|
||||
COLLABORATION SPECTRUM
|
||||
════════════════════════════════════════════
|
||||
|
||||
Awareness Coordination Sync
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│Presence│ │Cursors │ │ CRDT │
|
||||
│ "3 │ │ Multi │ │Conflict│
|
||||
│online" │ │ select │ │ free │
|
||||
└────────┘ └────────┘ └────────┘
|
||||
│ │ │
|
||||
trivial moderate complex
|
||||
|
||||
Where's your head at?
|
||||
```
|
||||
|
||||
**User brings a specific problem:**
|
||||
```
|
||||
User: The auth system is a mess
|
||||
|
||||
You: [reads codebase]
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CURRENT AUTH FLOW │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Google │ │ GitHub │ │ Email │
|
||||
│ OAuth │ │ OAuth │ │ Magic │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
└─────────────┼─────────────┘
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Session │
|
||||
└─────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Perms │
|
||||
└───────────┘
|
||||
|
||||
I see three tangles. Which one's burning?
|
||||
```
|
||||
|
||||
**User is stuck mid-implementation:**
|
||||
```
|
||||
User: /opsx:explore add-auth-system
|
||||
The OAuth integration is more complex than expected
|
||||
|
||||
You: [reads change artifacts]
|
||||
|
||||
You're on task 4: "Implement OAuth flow"
|
||||
|
||||
Let me trace what's involved...
|
||||
|
||||
[draws diagram, explores options, suggests paths]
|
||||
|
||||
Want to update the design to reflect this?
|
||||
Or add a spike task to investigate?
|
||||
```
|
||||
|
||||
**User wants to compare options:**
|
||||
```
|
||||
User: Should we use Postgres or SQLite?
|
||||
|
||||
You: Generic answer is boring. What's the context?
|
||||
|
||||
User: A CLI tool that tracks local dev environments
|
||||
|
||||
You: That changes everything.
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Key constraints:
|
||||
• No daemon running
|
||||
• Must work offline
|
||||
• Single user
|
||||
|
||||
SQLite Postgres
|
||||
Deployment embedded ✓ needs server ✗
|
||||
Offline yes ✓ no ✗
|
||||
Single file yes ✓ no ✗
|
||||
|
||||
SQLite. Not even close.
|
||||
|
||||
Unless... is there a sync component?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When it feels like things are crystallizing, you might summarize:
|
||||
|
||||
```
|
||||
## What We Figured Out
|
||||
|
||||
**The problem**: [crystallized understanding]
|
||||
|
||||
**The approach**: [if one emerged]
|
||||
|
||||
**Open questions**: [if any remain]
|
||||
|
||||
**Next steps** (if ready):
|
||||
- Create a change proposal
|
||||
- Keep exploring: just keep talking
|
||||
```
|
||||
|
||||
But this summary is optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
110
.claude/skills/openspec-propose/SKILL.md
Normal file
110
.claude/skills/openspec-propose/SKILL.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
name: openspec-propose
|
||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
24
.editorconfig
Normal file
24
.editorconfig
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Configuration for EditorConfig
|
||||
# https://EditorConfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{py,pyi}]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
[*.{xml,yml,yaml,json,js,css,scss,md,rst}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.{md,rst}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
22
.env.example
Normal file
22
.env.example
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Local environment for docker-compose. Copy to `.env` and adjust the paths:
|
||||
#
|
||||
# cp .env.example .env
|
||||
#
|
||||
# `.env` is gitignored: every developer points these at their own checkouts.
|
||||
# docker-compose reads it automatically. For manual `docker-compose run` calls
|
||||
# that need $ODOO_ADDONS_PATH in the shell, load it first:
|
||||
#
|
||||
# set -a; . ./.env; set +a
|
||||
|
||||
# Host directory holding the read-only Odoo/OCA sources (OCB + the OCA repos
|
||||
# listed in oca_dependencies.txt). Each repo is mounted at /mnt/oca/<repo>.
|
||||
ODOO_SRC=/media/snt/data/odoo
|
||||
|
||||
# Host ports published by the odoo service (web / longpolling).
|
||||
ODOO_HTTP_PORT=8069
|
||||
ODOO_LONGPOLLING_PORT=8072
|
||||
|
||||
# Addons path *inside* the container. Single source of truth: used by the
|
||||
# `command` in docker-compose.yml and by manual `docker-compose run`
|
||||
# invocations (`odoo --addons-path=$ODOO_ADDONS_PATH ...`).
|
||||
ODOO_ADDONS_PATH=/mnt/extra-addons,/mnt/oca/account-invoice-reporting,/mnt/oca/account-invoicing,/mnt/oca/bank-payment,/mnt/oca/community-data-files,/mnt/oca/l10n-spain,/mnt/oca/pos,/mnt/oca/product-attribute,/mnt/oca/purchase-workflow,/mnt/oca/sale-workflow,/mnt/oca/stock-logistics-workflow,/usr/lib/python3/dist-packages/odoo/addons
|
||||
31
.flake8
Normal file
31
.flake8
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# See https://github.com/OCA/maintainer-tools/issues/38
|
||||
# E123, E133, E226, E241, E242 are ignored by default
|
||||
# E203 for black (whitespace before : in slice)
|
||||
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
max-complexity = 16
|
||||
# B = bugbear
|
||||
# B9 = bugbear opinionated (incl line length)
|
||||
select = C,E,F,W,B,B9
|
||||
# E203: whitespace before ':' (black-compatible)
|
||||
# E501: line too long (black-compatible)
|
||||
# W503: line break before binary operator (black-compatible)
|
||||
# B950: line too long (soft limit, complements B)
|
||||
ignore = E203,E501,W503,B950
|
||||
# OCA standard per-file ignores:
|
||||
# - __init__.py imports submodules that are intentionally "unused" (F401)
|
||||
# - __manifest__.py is a bare dict literal, not a useless expression (B018)
|
||||
per-file-ignores =
|
||||
__init__.py: F401
|
||||
__manifest__.py: B018
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
.tox,
|
||||
.eggs,
|
||||
*.egg,
|
||||
build,
|
||||
.venv,
|
||||
.env,
|
||||
setup
|
||||
36
.gitea/issue-bot.md
Normal file
36
.gitea/issue-bot.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
Este repo no es de un único cliente: son los addons de Odoo 18 Community que
|
||||
Criptomart comparte entre varias instancias (Elika Bilbo y otros grupos y
|
||||
cooperativas de consumo, y Ecocentral, que lo usa como dependencia). Hay
|
||||
addons de precios y productos, compra colectiva para grupos de consumo
|
||||
(`website_sale_aplicoop`), socias y cuotas, TPV (CashDro), logística y
|
||||
contabilidad.
|
||||
|
||||
- Un cambio aquí llega a todas las instancias que instalan ese addon. Cuida la
|
||||
compatibilidad: nada de suponer la configuración, los datos ni el idioma de
|
||||
un cliente concreto, y nada que rompa los datos que ya existen (sube la
|
||||
versión del manifest y añade un script de migración cuando haga falta). Si
|
||||
lo que se pide solo tiene sentido para un cliente, propón hacerlo
|
||||
configurable o en un addon aparte, y dilo.
|
||||
- La rama `18.0` es la de trabajo. `16.0` y `17.0` son de versiones anteriores
|
||||
de Odoo: si una incidencia se refiere a ellas, dilo en lugar de resolverla
|
||||
aquí. Si se refiere a un addon propio de un cliente (por ejemplo los `eco_*`
|
||||
de ecocentral), también.
|
||||
- Las fuentes de Odoo y OCA traen un clon de este mismo repo (`addons-cm`) de
|
||||
la última actualización semanal: el código que vale es el del árbol de
|
||||
trabajo, no ese clon.
|
||||
- Convenciones de código: `.github/copilot-instructions.md`. Lo principal: la
|
||||
lógica de negocio va en `product.product` (en `product.template`, campos
|
||||
`related`); nada de lógica en QWeb ni en JavaScript, se prepara en Python;
|
||||
sin `_()` en la definición de los campos; no se modifican los addons de OCA
|
||||
ni el core, se heredan desde un addon propio.
|
||||
- Idioma de los textos: el código, los comentarios y los textos que ve el
|
||||
usuario (campos, vistas, mensajes) se escriben en inglés, y sus traducciones
|
||||
van en `i18n/<idioma>.po`. Castellano (`es.po`) y euskera (`eu.po`) son
|
||||
obligatorios; hay addons con `ca.po` y `gl.po`. Los `.po` se editan con
|
||||
`polib`, nunca con `msgmerge`, que los corrompe.
|
||||
- Las personas que abren incidencias escriben en castellano o en euskera, y
|
||||
usan Odoo en castellano o en euskera: nombra los menús y campos como salen
|
||||
en pantalla en su idioma.
|
||||
- Commits con el estilo `[FIX] website_sale_aplicoop: show the full product
|
||||
name on the cards` (etiquetas `[ADD] [FIX] [IMP] [REF] [REM] [I18N] [DOC]`),
|
||||
en inglés; título y descripción del PR también en inglés.
|
||||
96
.gitea/workflows/ci.yml
Normal file
96
.gitea/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
name: CI
|
||||
# Las comillas de "18.0" hacen falta: sin ellas YAML lo lee como un número y
|
||||
# Forgejo descarta el workflow entero, sin avisar.
|
||||
on:
|
||||
push:
|
||||
branches: ["18.0"]
|
||||
pull_request:
|
||||
branches: ["18.0"]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
# Misma imagen que el bot de incidencias: ya trae Python y pre-commit, y
|
||||
# el runner le monta la caché con los entornos de los hooks. Así el job no
|
||||
# depende de descargar Python de GitHub en cada ejecución.
|
||||
runs-on: issue-bot
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run pre-commit
|
||||
run: pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
test:
|
||||
# Misma imagen que el bot de incidencias: trae las dependencias de Odoo y
|
||||
# Chromium, y el runner le monta OCB y los repos OCA en /odoo-src. Ver
|
||||
# tools/issue_bot/README.md.
|
||||
runs-on: issue-bot
|
||||
# El runner lanza los contenedores en la red del host (hace falta para
|
||||
# llegar a Forgejo en localhost), y ahí los servicios no tienen nombre: se
|
||||
# llega a PostgreSQL por 127.0.0.1. Escucha solo en esa dirección y en un
|
||||
# puerto propio para no quedar expuesto ni chocar con otro PostgreSQL de
|
||||
# la máquina. El de ecocentral usa el 54329, y su runner y el de este
|
||||
# repo son registros distintos en la misma máquina: un job test de cada
|
||||
# uno puede correr a la vez.
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: odoo
|
||||
POSTGRES_PASSWORD: odoo
|
||||
POSTGRES_DB: postgres
|
||||
PGPORT: "54330"
|
||||
cmd: ["postgres", "-c", "listen_addresses=127.0.0.1"]
|
||||
env:
|
||||
PGHOST: 127.0.0.1
|
||||
PGPORT: "54330"
|
||||
PGUSER: odoo
|
||||
PGPASSWORD: odoo
|
||||
PGDATABASE: test
|
||||
ODOO_SRC: /odoo-src/18.0
|
||||
# El runner es el de ecocentral, y su /odoo-src/18.0 trae un clon de
|
||||
# este repo (ecocentral depende de él): se prueba el checkout del PR,
|
||||
# no ese clon.
|
||||
ODOO_SRC_SKIP: addons-cm
|
||||
# Se prueban cada uno en su propia base de datos: instalados junto
|
||||
# al resto les cambian el comportamiento. website_sale_disable_cart
|
||||
# quita el carrito a todos los sitios web y rompe, por ejemplo, el
|
||||
# alta con cuota de website_membership_signup_required.
|
||||
ISOLATE: website_sale_disable_cart
|
||||
# Si los tests pasan, deja aquí el .pot de cada addon para el job review.
|
||||
# Está en la caché que el runner monta en todos los jobs.
|
||||
BOT_POT_DIR: /root/.cache/eco-pr-bot/pot
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tests de los addons
|
||||
run: python3 tools/ci/run_tests.py
|
||||
|
||||
review:
|
||||
# Revisión del PR por IA (seguridad, idiomas y flujos de Odoo): solo en los
|
||||
# PR que no están marcados como WIP, y después de pasar lint y test. El
|
||||
# resto de filtros (etiqueta no-bot, commit ya revisado) los aplica
|
||||
# review.py. Ver tools/pr_bot/README.md.
|
||||
needs: [lint, test]
|
||||
if: github.event_name == 'pull_request' && !github.event.pull_request.draft
|
||||
runs-on: issue-bot
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# review.py pone las credenciales de la cuenta del bot al hacer push.
|
||||
persist-credentials: false
|
||||
- name: Run bot
|
||||
env:
|
||||
BOT_FORGEJO_URL: ${{ github.server_url }}
|
||||
BOT_FORGEJO_REPO: ${{ github.repository }}
|
||||
# La rama por defecto del repo es 16.0: sin esto el bot tomaría
|
||||
# esa como base.
|
||||
BOT_BASE_BRANCH: "18.0"
|
||||
BOT_FORGEJO_TOKEN: ${{ secrets.BOT_FORGEJO_TOKEN }}
|
||||
# Basta con uno de los dos.
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
BOT_REPO_DIR: ${{ github.workspace }}
|
||||
BOT_ODOO_SRC: /odoo-src/18.0:/odoo-src/18.0-oca
|
||||
BOT_POT_DIR: /root/.cache/eco-pr-bot/pot
|
||||
run: >-
|
||||
python3 tools/pr_bot/review.py run
|
||||
"${{ github.event.pull_request.number }}" --ci-passed
|
||||
40
.gitea/workflows/issue-bot.yml
Normal file
40
.gitea/workflows/issue-bot.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Bot de incidencias
|
||||
run-name: "#${{ github.event.issue.number }} ${{ github.event.issue.title }}"
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
bot:
|
||||
# Imagen propia con Claude Code y pre-commit ya instalados; el runner monta
|
||||
# las fuentes de Odoo en /odoo-src. Ver tools/issue_bot/README.md.
|
||||
runs-on: issue-bot
|
||||
# Lo que publica la cuenta del bot también dispara el evento: sus respuestas
|
||||
# y el eco de los correos de notificación. Se corta aquí; el resto de
|
||||
# filtros (PR, cerradas, etiqueta no-bot) los aplica bot.py.
|
||||
if: github.actor != 'claudia_eco'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# bot.py pone las credenciales de la cuenta del bot al hacer push.
|
||||
persist-credentials: false
|
||||
- name: Run bot
|
||||
env:
|
||||
# Con prefijo BOT_: el runner define sus propias FORGEJO_* y
|
||||
# GITHUB_*, entre ellas un FORGEJO_TOKEN que no es de ningún usuario.
|
||||
BOT_FORGEJO_URL: ${{ github.server_url }}
|
||||
BOT_FORGEJO_REPO: ${{ github.repository }}
|
||||
# La rama por defecto del repo es 16.0: sin esto el bot tomaría
|
||||
# esa como base.
|
||||
BOT_BASE_BRANCH: "18.0"
|
||||
BOT_FORGEJO_TOKEN: ${{ secrets.BOT_FORGEJO_TOKEN }}
|
||||
# Basta con uno de los dos.
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
BOT_REPO_DIR: ${{ github.workspace }}
|
||||
# Dependencias del proyecto y, aparte, el resto de OCA.
|
||||
BOT_ODOO_SRC: /odoo-src/18.0:/odoo-src/18.0-oca
|
||||
run: python3 tools/issue_bot/bot.py run "${{ github.event.issue.number }}"
|
||||
36
.gitea/workflows/pr-bot.yml
Normal file
36
.gitea/workflows/pr-bot.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: Bot de revisión
|
||||
run-name: "#${{ github.event.pull_request.number }} ${{ github.event.pull_request.title }}"
|
||||
# La revisión normal es el job review de ci.yml, que corre detrás de lint y
|
||||
# test. Este workflow cubre los dos cambios que no relanzan el CI: quitar la
|
||||
# marca WIP (es editar el título) y poner la etiqueta i18n. review.py comprueba
|
||||
# que el CI ya había pasado para ese commit y que no está revisado.
|
||||
on:
|
||||
pull_request:
|
||||
types: [edited, labeled]
|
||||
branches: ["18.0"]
|
||||
|
||||
jobs:
|
||||
bot:
|
||||
runs-on: issue-bot
|
||||
if: "!github.event.pull_request.draft"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# review.py pone las credenciales de la cuenta del bot al hacer push.
|
||||
persist-credentials: false
|
||||
- name: Run bot
|
||||
env:
|
||||
BOT_FORGEJO_URL: ${{ github.server_url }}
|
||||
BOT_FORGEJO_REPO: ${{ github.repository }}
|
||||
# La rama por defecto del repo es 16.0: sin esto el bot tomaría
|
||||
# esa como base.
|
||||
BOT_BASE_BRANCH: "18.0"
|
||||
BOT_FORGEJO_TOKEN: ${{ secrets.BOT_FORGEJO_TOKEN }}
|
||||
# Basta con uno de los dos.
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
BOT_REPO_DIR: ${{ github.workspace }}
|
||||
BOT_ODOO_SRC: /odoo-src/18.0:/odoo-src/18.0-oca
|
||||
BOT_POT_DIR: /root/.cache/eco-pr-bot/pot
|
||||
run: python3 tools/pr_bot/review.py run "${{ github.event.pull_request.number }}"
|
||||
773
.github/copilot-instructions.md
vendored
Normal file
773
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
# ⚠️ Addons OCA Originales y OCB (Odoo)
|
||||
|
||||
No modificar el directorio de fuentes de OCB (`ocb/`) ni los siguientes addons OCA originales:
|
||||
|
||||
- `product_main_seller`
|
||||
- `account_invoice_triple_discount`
|
||||
- `product_get_price_helper`
|
||||
- `product_price_category`
|
||||
- `purchase_triple_discount`
|
||||
- `base_bank_from_iban`
|
||||
- `l10n_es_partner`
|
||||
|
||||
Estos módulos y el core de Odoo (OCB) solo están para referencia y herencia de nuestros addons custom. Cualquier cambio debe hacerse en los addons propios, nunca en los OCA originales ni en el core OCB.
|
||||
|
||||
# AI Agent Skills & Prompt Guidance
|
||||
|
||||
Para máxima productividad y calidad, los agentes AI deben seguir estas pautas y consultar los archivos de skills detallados:
|
||||
|
||||
## 1. Skills por Lenguaje/Stack
|
||||
|
||||
- **Python/Odoo ORM**: `.github/skills/python.md`
|
||||
- **XML/Odoo Views**: `.github/skills/xml.md`
|
||||
- **HTML/CSS/QWeb**: `.github/skills/html_css.md`
|
||||
- **JavaScript/Odoo Web**: `.github/skills/javascript.md`
|
||||
|
||||
## 2. Ejecución de tests
|
||||
|
||||
Siempre usa:
|
||||
|
||||
```bash
|
||||
docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u addon_name
|
||||
```
|
||||
|
||||
> NOTA: Usa `docker-compose run` (no `exec`) para evitar efectos de caché.
|
||||
|
||||
## 3. Patrones y convenciones clave
|
||||
|
||||
- Sigue OCA guidelines y skills.
|
||||
- No pongas lógica en QWeb, solo acceso a datos preparados en el controller.
|
||||
- Usa `main_seller_id` en vez de `default_supplier_id` en product_main_seller.
|
||||
- Para bulk updates, prefiere `search().write()` sobre loops.
|
||||
|
||||
## 4. Errores comunes a evitar
|
||||
|
||||
- No uses `_()` en definiciones de campos, solo en métodos ejecutables.
|
||||
- No pongas lógica de negocio en JavaScript, solo en Python.
|
||||
- No uses estilos inline en QWeb salvo casos justificados.
|
||||
|
||||
## 5. Documentación y referencias
|
||||
|
||||
- Consulta este archivo para patrones globales.
|
||||
- Consulta los archivos de skills para detalles por lenguaje.
|
||||
- Para troubleshooting de templates, revisa `docs/QWEB_BEST_PRACTICES.md` y `docs/FINAL_SOLUTION_SUMMARY.md`.
|
||||
- **Para documentación de addons**: Sigue los estándares OCA descritos en `docs/OCA_DOCUMENTATION.md`
|
||||
- Estructura de directorios readme/ con fragmentos .rst
|
||||
- Créditos: Criptomart (autor) + Elika Bilbo (financiador)
|
||||
- Logo: CriptoMart en `static/description/icon.png` (ver `docs/LOGO_INSTRUCTIONS.md`)
|
||||
|
||||
# GitHub Copilot Instructions - Kidekoop Addons Custom
|
||||
|
||||
## Project Overview
|
||||
|
||||
Este repositorio contiene addons personalizados y modificados de Odoo 18.0. El proyecto combina:
|
||||
|
||||
- **OCB (Odoo Community Backports)**: Base de Odoo 18.0 community en `/ocb`
|
||||
- **Addons OCA**: Módulos de la comunidad OCA de los que heredan nuestros addons, modificados para adaptarse a nuestras necesidades
|
||||
- **Addons Custom**: Módulos desarrollados por este proyecto
|
||||
|
||||
## Architecture & Stack
|
||||
|
||||
- **Odoo Version**: 18.0 (OCB)
|
||||
- **Python Version**: 3.10+
|
||||
- **Framework**: Odoo ORM
|
||||
- **Deployment**: Docker Compose
|
||||
- **DB**: PostgreSQL
|
||||
- **Languages**: Python, XML, JavaScript, QWeb
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Python Style
|
||||
|
||||
- Seguir **OCA guidelines** estrictamente
|
||||
- Usar **black** para formateo (configurado en `pyproject.toml`)
|
||||
- **isort** para ordenar imports (perfil black)
|
||||
- **flake8** para linting
|
||||
- **pylint** con pylint-odoo para verificaciones específicas de Odoo
|
||||
- Pre-commit hooks activos (ver `.pre-commit-config.yaml`)
|
||||
|
||||
### Odoo Conventions
|
||||
|
||||
- **Model names**: snake_case con punto (`product.price.category`)
|
||||
- **Class names**: PascalCase (`ProductPriceCategory`)
|
||||
- **File names**: snake_case (`product_price_category.py`)
|
||||
- **XML IDs**: módulo.nombre_descriptivo (`product_price_category.view_form`)
|
||||
- **Manifest**: Siempre `__manifest__.py`, nunca `__openerp__.py`
|
||||
|
||||
### XML/View Standards
|
||||
|
||||
- **Indent**: 4 espacios (no tabs)
|
||||
- **XPath**: Usar `position` explícito (`before`, `after`, `inside`, `replace`, `attributes`)
|
||||
- **Groups**: Referenciar grupos con módulo.xml_id (`sales_team.group_sale_manager`)
|
||||
- **Sequence**: Usar `sequence` attribute para ordenar campos en vistas
|
||||
|
||||
### Translation System
|
||||
|
||||
**IMPORTANTE**: El sistema de traducciones está funcionando correctamente. Seguir estas reglas:
|
||||
|
||||
1. **Estructura de carpeta i18n/**:
|
||||
|
||||
```
|
||||
addon_name/
|
||||
├── i18n/
|
||||
│ ├── es.po # Español (obligatorio)
|
||||
│ ├── eu.po # Euskera (obligatorio)
|
||||
│ └── addon_name.pot # Template (generado)
|
||||
```
|
||||
|
||||
2. **NO usar `_()` en definiciones de campos a nivel de módulo**:
|
||||
|
||||
```python
|
||||
# ❌ INCORRECTO - causa warnings
|
||||
from odoo import _
|
||||
name = fields.Char(string=_("Name"))
|
||||
|
||||
# ✅ CORRECTO - traducción se maneja por .po files
|
||||
name = fields.Char(string="Name")
|
||||
```
|
||||
|
||||
3. **Usar `_()` solo en métodos y código ejecutable**:
|
||||
|
||||
```python
|
||||
def action_confirm(self):
|
||||
message = _("Confirmed successfully")
|
||||
return {'warning': {'message': message}}
|
||||
```
|
||||
|
||||
4. **Generar/actualizar traducciones**:
|
||||
|
||||
```bash
|
||||
# Exportar términos a traducir
|
||||
Pedir al usuario generar a través de UI, no sabemos el método correcto para exportar SÓLO las cadenas del addon sin incluir todo el sistema.
|
||||
```
|
||||
|
||||
Usar sólo polib y apend cadenas en los archivos .po, msmerge corrompe los archivos.
|
||||
|
||||
```
|
||||
|
||||
5. **Formato de archivos .po**:
|
||||
- Encoding: UTF-8
|
||||
- Content-Type: text/plain; charset=UTF-8
|
||||
- Language codes: `es`, `eu`, `ca`, `gl`, `pt`, `fr`, `it`
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
|
||||
addons-cm/
|
||||
├── .github/ # GitHub configs
|
||||
│ └── copilot-instructions.md # Este archivo
|
||||
├── ocb/ # Odoo Community Backports (18.0)
|
||||
│ └── addons/ # Addons core de Odoo
|
||||
├── oca_dependencies.txt # Dependencias OCA
|
||||
├── requirements.txt # Dependencias Python
|
||||
├── docker-compose.yml # Configuración Docker
|
||||
├── Makefile # Comandos útiles
|
||||
│
|
||||
├── # === ADDONS OCA NO MODIFICADOS ===
|
||||
├── account_invoice_triple_discount/ # Triple descuento en facturas (OCA)
|
||||
├── purchase_triple_discount/ # Triple descuento en compras (OCA)
|
||||
├── product_get_price_helper/ # Helper de precios (OCA)
|
||||
├── product_main_seller/ # Proveedor principal (OCA)
|
||||
├── product_price_category/ # Categorías de precio (OCA)
|
||||
├── base_bank_from_iban/ # Banco desde IBAN (OCA)
|
||||
├── l10n_es_partner/ # Adaptación partners/bancos ES (OCA)
|
||||
│
|
||||
├── # === ADDONS CUSTOM ===
|
||||
├── # --- Precios y productos ---
|
||||
├── product_sale_price_from_pricelist/ # Auto-cálculo precio venta (+ descuentos supplierinfo)
|
||||
├── product_pricelist_total_margin/ # Margen total aditivo (markup / comercial) con límites
|
||||
├── product_price_category_supplier/ # Extensión categorías precio por proveedor
|
||||
├── product_origin_char/ # Campo origen texto libre por producto
|
||||
├── account_invoice_triple_discount_readonly/ # Fix bug acumulación triple descuento
|
||||
├── # --- Ventas y web ---
|
||||
├── website_sale_aplicoop/ # Sistema eskaera (compras grupo)
|
||||
├── website_sale_disable_cart/ # Tienda solo catálogo: desactiva el carrito estándar
|
||||
├── website_sale_lazy_loading/ # /shop bajo demanda: scroll infinito o botón "Cargar más"
|
||||
├── portal_event_registration/ # Portal: ver registros de eventos + adjuntos
|
||||
├── # --- Membresías ---
|
||||
├── membership_monthly_invoicing/ # Factura mensual de membresía por socio (cron)
|
||||
├── membership_expiry_reminder/ # Email recordatorio de renovación próxima
|
||||
├── # --- Logística y contabilidad ---
|
||||
├── stock_picking_batch_collect/ # Batch picking: columnas extra + vista operario (Basket Assembly)
|
||||
├── account_banking_mandate_batch/ # Crear/validar mandatos SEPA en bloque desde contactos
|
||||
├── l10n_es_edi_tbai_reagyp_recibidas/ # Fix TicketBAI REAGYP facturas recibidas (19 → 02)
|
||||
├── # --- UI de backend ---
|
||||
└── web_list_striped/ # Efecto cebra en todas las listas del backend (sólo CSS)
|
||||
|
||||
````
|
||||
|
||||
## Addon References
|
||||
|
||||
**Para arquitectura, detalles de implementación y uso específico de cada addon, consultar su `README.rst` (documentación oficial) o `README_DEV.md` (detalles técnicos).**
|
||||
|
||||
### Addons OCA No Modificados
|
||||
|
||||
- [account_invoice_triple_discount](../account_invoice_triple_discount/README.rst)
|
||||
- [purchase_triple_discount](../purchase_triple_discount/README.rst)
|
||||
- [product_get_price_helper](../product_get_price_helper/README.rst)
|
||||
- [product_main_seller](../product_main_seller/README.rst)
|
||||
- [product_price_category](../product_price_category/README.rst)
|
||||
- [base_bank_from_iban](../base_bank_from_iban/README.rst)
|
||||
- [l10n_es_partner](../l10n_es_partner/README.rst)
|
||||
|
||||
### Addons Custom Propios
|
||||
|
||||
**Precios y productos**
|
||||
|
||||
- [product_sale_price_from_pricelist](../product_sale_price_from_pricelist/README.rst) - Auto-precio basado en compra (+ descuentos de supplierinfo)
|
||||
- [product_pricelist_total_margin](../product_pricelist_total_margin/README.rst) - Margen total aditivo (markup / comercial) con límites globales
|
||||
- [product_price_category_supplier](../product_price_category_supplier/README.rst) - Gestión categorías por proveedor
|
||||
- [product_origin_char](../product_origin_char/README.rst) - Campo origen de texto libre por producto
|
||||
- [account_invoice_triple_discount_readonly](../account_invoice_triple_discount_readonly/README.rst) - Fix bug acumulación descuentos
|
||||
|
||||
**Ventas y web**
|
||||
|
||||
- [website_sale_aplicoop](../website_sale_aplicoop/README.rst) - Sistema eskaera completo
|
||||
- [website_sale_disable_cart](../website_sale_disable_cart/README.rst) - Tienda solo catálogo: oculta el carrito estándar y redirige `/shop/cart*`
|
||||
- [website_sale_lazy_loading](../website_sale_lazy_loading/README.rst) - `/shop` bajo demanda: scroll infinito o botón "Cargar más" en vez del paginador
|
||||
- [portal_event_registration](../portal_event_registration/README.rst) - Portal: registros de eventos + adjuntos al chatter
|
||||
|
||||
**Membresías**
|
||||
|
||||
- [membership_monthly_invoicing](../membership_monthly_invoicing/README.rst) - Factura mensual de membresía por socio activo (cron)
|
||||
- [membership_expiry_reminder](../membership_expiry_reminder/README.rst) - Email automático de recordatorio de renovación
|
||||
|
||||
**Logística y contabilidad**
|
||||
|
||||
- [stock_picking_batch_collect](../stock_picking_batch_collect/README.rst) - Columnas extra, resumen por producto y vista operario para batch picking
|
||||
- [web_list_striped](../web_list_striped/README.rst) - Efecto cebra en todas las listas del backend (sólo CSS)
|
||||
- [account_banking_mandate_batch](../account_banking_mandate_batch/README.rst) - Creación masiva de mandatos SEPA desde contactos
|
||||
- [l10n_es_edi_tbai_reagyp_recibidas](../l10n_es_edi_tbai_reagyp_recibidas/README.rst) - Fix TicketBAI REAGYP en facturas recibidas (clave régimen 19 → 02)
|
||||
|
||||
**Nota**: Todos los addons custom siguen la estructura OCA con readme/ fragmentos. Ver [docs/OCA_DOCUMENTATION.md](../docs/OCA_DOCUMENTATION.md) para detalles.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Iniciar entorno (puertos: 8070=web, 8073=longpolling)
|
||||
docker-compose up -d
|
||||
|
||||
# Actualizar addon
|
||||
docker-compose exec odoo odoo -d odoo -u addon_name --stop-after-init
|
||||
|
||||
# Ver logs
|
||||
docker-compose logs -f odoo
|
||||
|
||||
# Ejecutar tests
|
||||
docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u addon_name
|
||||
|
||||
# NOTA: Usa `docker-compose run` (no `exec`) para lanzar un contenedor limpio y evitar efectos de caché. Cambia `addon_name` por el módulo a testear.
|
||||
|
||||
# Acceder a shell de Odoo
|
||||
docker-compose exec odoo bash
|
||||
|
||||
# Acceder a PostgreSQL
|
||||
docker-compose exec db psql -U odoo -d odoo
|
||||
````
|
||||
|
||||
### Quality Checks
|
||||
|
||||
```bash
|
||||
# Ejecutar todos los checks (usa .pre-commit-config.yaml)
|
||||
pre-commit run --all-files
|
||||
|
||||
# O usar Makefile (ver `make help` para todos los comandos)
|
||||
make lint # Solo linting (pre-commit)
|
||||
make format # Formatear código (black + isort)
|
||||
make check-format # Verificar formateo sin modificar
|
||||
make flake8 # Ejecutar flake8
|
||||
make pylint # Ejecutar pylint (todos)
|
||||
make pylint-required # Solo verificaciones mandatorias
|
||||
make clean # Limpiar archivos temporales
|
||||
```
|
||||
|
||||
### Tools Configuration
|
||||
|
||||
- **black**: Line length 88, target Python 3.10+ (ver `pyproject.toml`)
|
||||
- **isort**: Profile black, sections: STDLIB > THIRDPARTY > ODOO > ODOO_ADDONS > FIRSTPARTY > LOCALFOLDER
|
||||
- **flake8**: Ver `.flake8` para reglas específicas
|
||||
- **pylint**: Configurado para Odoo con `pylint-odoo` plugin
|
||||
|
||||
### Testing
|
||||
|
||||
- Tests en `tests/` de cada addon
|
||||
- Naming: `test_*.py`
|
||||
- Herencia: `odoo.tests.common.TransactionCase`
|
||||
- Ejecutar: `--test-enable` flag
|
||||
|
||||
## Critical Architecture Patterns
|
||||
|
||||
### Product Variants Architecture
|
||||
|
||||
**IMPORTANTE**: Los campos de lógica de negocio SIEMPRE van en `product.product` (variantes), no en `product.template`:
|
||||
|
||||
```python
|
||||
# ✅ CORRECTO - Lógica en product.product
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
last_purchase_price_updated = fields.Boolean(default=False)
|
||||
list_price_theoritical = fields.Float(default=0.0)
|
||||
|
||||
def _compute_theoritical_price(self):
|
||||
for product in self:
|
||||
# Cálculo real por variante
|
||||
pass
|
||||
|
||||
# ✅ CORRECTO - Template solo tiene campos related
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
last_purchase_price_updated = fields.Boolean(
|
||||
related='product_variant_ids.last_purchase_price_updated',
|
||||
readonly=False
|
||||
)
|
||||
```
|
||||
|
||||
**Por qué**: Evita problemas con pricelists y reportes que operan a nivel de variante. Ver `product_sale_price_from_pricelist` como ejemplo.
|
||||
|
||||
### QWeb Template Best Practices
|
||||
|
||||
**CRÍTICO**: QWeb tiene limitaciones estrictas con lógica compleja. **Siempre mover lógica al controller**:
|
||||
|
||||
```python
|
||||
# ❌ MAL - QWeb no puede parsear esto
|
||||
# <t t-set="price" t-value="price_info.get('price') or product.list_price or 0"/>
|
||||
|
||||
# ✅ CORRECTO - Preparar datos en controller
|
||||
class WebsiteController:
|
||||
def _prepare_product_display_info(self, product, price_info):
|
||||
"""Pre-procesar todos los valores para QWeb."""
|
||||
price = price_info.get(product.id, {}).get('price') or product.list_price or 0.0
|
||||
return {
|
||||
'display_price': float(price),
|
||||
'safe_uom_category': product.uom_id.category_id.name or '',
|
||||
}
|
||||
|
||||
# En template: acceso simple, sin lógica
|
||||
# <span t-esc="product_display['display_price']"/>
|
||||
```
|
||||
|
||||
Ver [docs/QWEB_BEST_PRACTICES.md](../docs/QWEB_BEST_PRACTICES.md) para más detalles.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Extending Models
|
||||
|
||||
```python
|
||||
from odoo import models, fields, api
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
custom_field = fields.Char(string="Custom Field")
|
||||
|
||||
@api.depends('field1', 'field2')
|
||||
def _compute_custom(self):
|
||||
for record in self:
|
||||
record.custom_computed = record.field1 + record.field2
|
||||
```
|
||||
|
||||
### Creating Wizards (Transient Models)
|
||||
|
||||
```python
|
||||
class WizardModel(models.TransientModel):
|
||||
_name = 'wizard.model.name'
|
||||
_description = "Wizard Description"
|
||||
|
||||
def action_confirm(self):
|
||||
# Business logic
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
```
|
||||
|
||||
### Bulk Updates
|
||||
|
||||
```python
|
||||
# Prefer SQL-level updates for performance
|
||||
self.env['product.template'].search([
|
||||
('default_supplier_id', '=', partner_id)
|
||||
]).write({'price_category_id': category_id})
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
||||
```python
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Success'),
|
||||
'message': _('Operation completed'),
|
||||
'type': 'success', # or 'warning', 'danger', 'info'
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Pattern
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# En métodos de cálculo de precios, usar logging detallado:
|
||||
_logger.info(
|
||||
"[PRICE DEBUG] Product %s [%s]: base_price=%.2f, tax_amount=%.2f",
|
||||
product.default_code or product.name,
|
||||
product.id,
|
||||
base_price,
|
||||
tax_amount,
|
||||
)
|
||||
```
|
||||
|
||||
### Price Calculation Pattern
|
||||
|
||||
```python
|
||||
# Usar product_get_price_helper para cálculos consistentes
|
||||
partial_price = product._get_price(qty=1, pricelist=pricelist)
|
||||
base_price = partial_price.get('value', 0.0) or 0.0
|
||||
|
||||
# Siempre validar taxes
|
||||
if not product.taxes_id:
|
||||
raise UserError(_("No taxes defined for product %s") % product.name)
|
||||
```
|
||||
|
||||
## Dependencies Management
|
||||
|
||||
### OCA Dependencies (`oca_dependencies.txt`)
|
||||
|
||||
```
|
||||
account-invoicing
|
||||
product-attribute
|
||||
purchase-workflow
|
||||
sale-workflow
|
||||
```
|
||||
|
||||
### Python Dependencies (`requirements.txt`)
|
||||
|
||||
- Versiones específicas para evitar breaking changes
|
||||
- Incluir herramientas de desarrollo (linters, etc)
|
||||
|
||||
## Security & Access Rights
|
||||
|
||||
### Grupos Comunes
|
||||
|
||||
- `base.group_user` - Usuario interno
|
||||
- `base.group_system` - Administrador
|
||||
- `sales_team.group_sale_manager` - Manager de ventas
|
||||
- `sales_team.group_sale_salesman` - Vendedor
|
||||
- `purchase.group_purchase_manager` - Manager de compras
|
||||
|
||||
### Security Files
|
||||
|
||||
```xml
|
||||
<!-- security/ir.model.access.csv -->
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_model_user,model.name.user,model_model_name,base.group_user,1,1,1,0
|
||||
```
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Translation Warnings
|
||||
|
||||
**Problem**: `WARNING: _() called at import time at module...`
|
||||
**Solution**: Remove `_()` from field definitions, use only in methods
|
||||
|
||||
### XPath Not Found
|
||||
|
||||
**Problem**: `Element ... not found in parent view`
|
||||
**Solution**: Check view inheritance chain, verify XML IDs are correct
|
||||
|
||||
### Discount Reset Issue
|
||||
|
||||
**Problem**: Multiple discounts being accumulated in discount1
|
||||
**Solution**: Use `account_invoice_triple_discount_readonly` addon
|
||||
|
||||
### Price Calculation
|
||||
|
||||
**Problem**: Prices not updating from pricelist
|
||||
**Solution**:
|
||||
|
||||
1. Use `product_sale_price_from_pricelist` with proper configuration
|
||||
2. Set pricelist in Settings > Sales > Automatic Price Configuration
|
||||
3. Ensure `last_purchase_price_compute_type` is NOT set to `manual_update`
|
||||
4. Verify product has taxes configured (required for price calculation)
|
||||
|
||||
### Product Variant Issues
|
||||
|
||||
**Problem**: Computed fields not working in pricelists/reports
|
||||
**Solution**: Move business logic from `product.template` to `product.product` and use `related` fields in template
|
||||
|
||||
### Manifest Dependencies
|
||||
|
||||
**Problem**: Module not loading, dependency errors
|
||||
**Solution**: Check both `__manifest__.py` depends AND `oca_dependencies.txt` for OCA repos
|
||||
|
||||
### QWeb Template Errors
|
||||
|
||||
**Problem**: `TypeError: 'NoneType' object is not callable` in templates
|
||||
**Solution**:
|
||||
|
||||
1. Move complex logic from template to controller
|
||||
2. Use simple attribute access in templates (no conditionals)
|
||||
3. Pre-process all display values in Python
|
||||
4. See [docs/QWEB_BEST_PRACTICES.md](../docs/QWEB_BEST_PRACTICES.md) for patterns
|
||||
|
||||
**Example Pattern**:
|
||||
|
||||
```python
|
||||
# Controller: prepare clean data
|
||||
def _prepare_display_info(self, product):
|
||||
return {
|
||||
'price': product.price or 0.0,
|
||||
'uom': product.uom_id.name or '',
|
||||
}
|
||||
|
||||
# Template: use simple access
|
||||
<span t-esc="display_info['price']"/>
|
||||
```
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Test business logic in isolation
|
||||
- Mock external dependencies
|
||||
- Use `TransactionCase` for DB tests
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Test workflow end-to-end
|
||||
- Verify computed fields
|
||||
- Check access rights
|
||||
|
||||
### UI Tests (Tours)
|
||||
|
||||
```javascript
|
||||
odoo.define("module.tour", function (require) {
|
||||
"use strict";
|
||||
var tour = require("web_tour.tour");
|
||||
tour.register(
|
||||
"tour_name",
|
||||
{
|
||||
test: true,
|
||||
url: "/web",
|
||||
},
|
||||
[
|
||||
// Tour steps
|
||||
],
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable Developer Mode
|
||||
|
||||
```
|
||||
Settings > Activate Developer Mode
|
||||
```
|
||||
|
||||
### Check Logs
|
||||
|
||||
```bash
|
||||
docker-compose logs -f odoo | grep ERROR
|
||||
docker-compose logs -f odoo | grep addon_name
|
||||
```
|
||||
|
||||
### Python Debugger
|
||||
|
||||
```python
|
||||
import pdb; pdb.set_trace() # Set breakpoint
|
||||
```
|
||||
|
||||
### Performance Profiling
|
||||
|
||||
```bash
|
||||
--log-level=debug_sql # Show SQL queries
|
||||
```
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### OCA README Structure
|
||||
|
||||
Cada addon custom sigue la **estructura OCA** con fragmentos en `readme/`:
|
||||
|
||||
- `readme/DESCRIPTION.rst` - Descripción y características
|
||||
- `readme/INSTALL.rst` - Instalación y dependencias
|
||||
- `readme/CONFIGURE.rst` - Configuración post-instalación
|
||||
- `readme/USAGE.rst` - Guía de uso
|
||||
- `readme/CONTRIBUTORS.rst` - Contribuidores
|
||||
- `readme/CREDITS.rst` - Créditos (Criptomart + Elika Bilbo)
|
||||
- `README.rst` - README oficial generado/manual
|
||||
- `README_DEV.md` - Notas técnicas para desarrolladores
|
||||
|
||||
**Ver**: [docs/OCA_DOCUMENTATION.md](../docs/OCA_DOCUMENTATION.md) para estructura completa.
|
||||
|
||||
### **manifest**.py Structure
|
||||
|
||||
Todos los addons custom deben seguir esta estructura:
|
||||
|
||||
```python
|
||||
# Copyright YEAR - Today AUTHOR
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{ # noqa: B018
|
||||
"name": "Addon Name",
|
||||
"version": "18.0.X.Y.Z", # X=major, Y=minor, Z=patch
|
||||
"category": "category_name",
|
||||
"summary": "Short description",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
"maintainers": ["Criptomart"],
|
||||
"website": "https://github.com/OCA/repo",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"base",
|
||||
# Lista ordenada alfabéticamente
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/actions.xml",
|
||||
"views/menu.xml",
|
||||
"views/model_views.xml",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Code Comments
|
||||
|
||||
- Docstrings en clases y métodos públicos
|
||||
- Comentarios inline para lógica compleja
|
||||
- TODOs con contexto completo
|
||||
- Logging detallado en operaciones de precios/descuentos
|
||||
|
||||
## Version Control
|
||||
|
||||
### Commit Messages
|
||||
|
||||
```
|
||||
[TAG] module: Brief description
|
||||
|
||||
Longer explanation if needed
|
||||
```
|
||||
|
||||
Tags: `[ADD]`, `[FIX]`, `[IMP]`, `[REF]`, `[REM]`, `[I18N]`, `[DOC]`
|
||||
|
||||
### Branch Strategy
|
||||
|
||||
- `main` - Production ready
|
||||
- `dev` - Development
|
||||
- `feature/*` - New features
|
||||
- `fix/*` - Bug fixes
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Use `@api.depends` correctamente para computed fields
|
||||
- Prefer `search()` + `write()` sobre loops con `write()`
|
||||
- Use `create()` con lista de vals para bulk creates
|
||||
- Indexes en campos frecuentemente buscados
|
||||
- Avoid N+1 queries con `prefetch`
|
||||
|
||||
## Key Business Features
|
||||
|
||||
### Eskaera System (website_sale_aplicoop)
|
||||
|
||||
Sistema completo de compras colaborativas para cooperativas de consumo:
|
||||
|
||||
- **Group Orders**: Pedidos grupales con estados (draft → confirmed → collected → completed)
|
||||
- **Separate Carts**: Carrito independiente por miembro y por grupo
|
||||
- **Cutoff Dates**: Validación de fechas límite para pedidos
|
||||
- **Pickup Management**: Gestión de días de recogida
|
||||
- **Lazy Loading**: Carga configurable de productos (v18.0.1.3.0+)
|
||||
- **Multi-language**: ES, EU, CA, GL, PT, FR, IT
|
||||
- **Member Tracking**: Gestión de miembros activos/inactivos por grupo
|
||||
|
||||
**Flujo típico**:
|
||||
|
||||
1. Administrador crea grupo order con fechas (collection, cutoff, pickup)
|
||||
2. Miembros añaden productos a su carrito individual
|
||||
3. Sistema valida cutoff date antes de confirmar
|
||||
4. Notificaciones automáticas al cambiar estados
|
||||
5. Tracking de fulfillment por miembro
|
||||
|
||||
**Configuración Lazy Loading** (v18.0.1.3.0+):
|
||||
|
||||
```
|
||||
Settings > Website > Shop Performance
|
||||
[✓] Enable Lazy Loading
|
||||
[20] Products Per Page
|
||||
```
|
||||
|
||||
**Mejoras Recientes**:
|
||||
|
||||
- v18.0.1.3.1: Fixes críticos de cálculo de fechas
|
||||
- v18.0.1.3.0: Lazy loading, mejora de rendimiento de 10-20s → 500-800ms
|
||||
- Refactor de template rendering: Mover lógica QWeb al controller
|
||||
|
||||
Ver [website_sale_aplicoop/README.rst](../website_sale_aplicoop/README.rst) y [docs/LAZY_LOADING.md](../docs/LAZY_LOADING.md) para detalles.
|
||||
|
||||
### Triple Discount System
|
||||
|
||||
Todos los documentos de compra/venta soportan 3 descuentos consecutivos:
|
||||
|
||||
```python
|
||||
# Ejemplo: Precio = 600.00
|
||||
# Desc. 1 = 50% → 300.00
|
||||
# Desc. 2 = 50% → 150.00
|
||||
# Desc. 3 = 50% → 75.00
|
||||
```
|
||||
|
||||
**IMPORTANTE**: Usar `account_invoice_triple_discount_readonly` para evitar bug de acumulación de descuentos.
|
||||
|
||||
### Automatic Pricing System
|
||||
|
||||
`product_sale_price_from_pricelist` calcula automáticamente precio de venta basado en:
|
||||
|
||||
- Último precio de compra (`last_purchase_price_received`)
|
||||
- Tipo de cálculo de descuentos (`last_purchase_price_compute_type`)
|
||||
- Pricelist configurado en Settings
|
||||
- Impuestos del producto
|
||||
|
||||
**Configuración crítica**:
|
||||
|
||||
```python
|
||||
# En Settings > Sales > Automatic Price Configuration
|
||||
product_pricelist_automatic = [ID_pricelist]
|
||||
|
||||
# En producto
|
||||
last_purchase_price_compute_type != "manual_update" # Para auto-cálculo
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **OCA Guidelines**: https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/CONTRIBUTING.rst
|
||||
- **Odoo Documentation**: https://www.odoo.com/documentation/18.0/
|
||||
- **OCB Repository**: https://github.com/OCA/OCB
|
||||
- **OCA Repositories**: https://github.com/OCA/
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-06-04
|
||||
**Odoo Version**: 18.0
|
||||
**Python Version**: 3.10+
|
||||
|
||||
## Recent Changes Summary
|
||||
|
||||
- **2026-06-04**: Nuevos addons custom documentados: `membership_monthly_invoicing`,
|
||||
`membership_expiry_reminder`, `account_banking_mandate_batch`, `portal_event_registration`,
|
||||
`product_pricelist_total_margin`, `product_origin_char`, `l10n_es_edi_tbai_reagyp_recibidas`.
|
||||
`product_origin` (OCA) sustituido por `product_origin_char` (custom).
|
||||
- **2026-06-04**: `product_sale_price_from_pricelist` aplica descuentos de proveedor desde `supplierinfo`.
|
||||
- **2026-08-27**: `stock_picking_batch_custom` renombrado a `stock_picking_batch_collect`
|
||||
(v18.0.2.0.0). Se invierte la dependencia: ya no depende de `website_sale_aplicoop`; es este
|
||||
el que depende de él e inyecta las columnas de grupo de consumo. El efecto cebra global se
|
||||
extrae al nuevo addon `web_list_striped`. El renombrado en BD lo hace un `pre_init_hook`.
|
||||
- **2026-06**: `stock_picking_batch_custom` — vista operario a pantalla completa (Basket Assembly),
|
||||
ordenación de operaciones por categoría/producto/partner.
|
||||
- **2026-02-18**: Refactor `product_main_seller` - Remover alias innecesario `default_supplier_id`
|
||||
- **2026-02-16**: v18.0.1.3.1 fixes críticos de cálculo de fechas en Eskaera
|
||||
- **2026-02-12**: v18.0.1.3.0 Lazy loading y fixes de template rendering QWeb
|
||||
- **2026-02-02**: UI improvements y date calculation fixes
|
||||
149
.github/prompts/opsx-apply.prompt.md
vendored
Normal file
149
.github/prompts/opsx-apply.prompt.md
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
description: Implement tasks from an OpenSpec change (Experimental)
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! You can archive this change with `/opsx:archive`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
154
.github/prompts/opsx-archive.prompt.md
vendored
Normal file
154
.github/prompts/opsx-archive.prompt.md
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
description: Archive a completed change in the experimental workflow
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / sync skipped / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
```
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** Sync skipped (user chose to skip)
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta spec sync was skipped (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
```
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
```
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
170
.github/prompts/opsx-explore.prompt.md
vendored
Normal file
170
.github/prompts/opsx-explore.prompt.md
vendored
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||
- A vague idea: "real-time collaboration"
|
||||
- A specific problem: "the auth system is getting unwieldy"
|
||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||
- A comparison: "postgres vs sqlite for this"
|
||||
- Nothing (just enter explore mode)
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
If the user mentioned a specific change name, read its artifacts for context.
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
103
.github/prompts/opsx-propose.prompt.md
vendored
Normal file
103
.github/prompts/opsx-propose.prompt.md
vendored
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
description: Propose a new change - create it and generate all artifacts in one step
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
17
.github/skills/html_css.md
vendored
Normal file
17
.github/skills/html_css.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Skill: HTML & CSS (Odoo/QWeb)
|
||||
|
||||
## Estilo y convenciones
|
||||
- Usa HTML5 válido en QWeb y vistas web.
|
||||
- No uses lógica condicional en QWeb, solo acceso a atributos.
|
||||
- Prepara todos los datos en el controller Python.
|
||||
- CSS: usa clases BEM o utilitarias, nunca estilos inline.
|
||||
- Indentación: 4 espacios.
|
||||
|
||||
## Patrones clave
|
||||
- Templates en `views/*.xml` o `static/src/xml/`.
|
||||
- Para estilos, usa `static/src/css/` y carga en manifest si es necesario.
|
||||
- Para responsive, usa clases utilitarias o Bootstrap si está disponible.
|
||||
|
||||
## Errores comunes
|
||||
- No pongas lógica en QWeb, solo acceso a datos.
|
||||
- No uses estilos inline salvo casos muy justificados.
|
||||
18
.github/skills/javascript.md
vendored
Normal file
18
.github/skills/javascript.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Skill: JavaScript (Odoo Web/Frontend)
|
||||
|
||||
## Estilo y convenciones
|
||||
- Usa ES6+ (let/const, arrow functions, etc).
|
||||
- Sigue el patrón Odoo: módulos AMD (`odoo.define`).
|
||||
- Linting con ESLint (`.eslintrc.js` en raíz).
|
||||
- No mezcles lógica de negocio en JS, solo UI y eventos.
|
||||
- Usa comentarios para explicar hacks o workarounds.
|
||||
|
||||
## Patrones clave
|
||||
- Scripts en `static/src/js/`.
|
||||
- Registra tours de UI con `web_tour.tour`.
|
||||
- Usa eventos delegados para elementos dinámicos (lazy loading, infinite scroll).
|
||||
- Para AJAX, usa los helpers de Odoo (`ajax.jsonRpc`).
|
||||
|
||||
## Errores comunes
|
||||
- No pongas lógica de negocio en JS, solo en Python.
|
||||
- Reatacha listeners tras manipular el DOM dinámicamente.
|
||||
156
.github/skills/openspec-apply-change/SKILL.md
vendored
Normal file
156
.github/skills/openspec-apply-change/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
114
.github/skills/openspec-archive-change/SKILL.md
vendored
Normal file
114
.github/skills/openspec-archive-change/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
name: openspec-archive-change
|
||||
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
288
.github/skills/openspec-explore/SKILL.md
vendored
Normal file
288
.github/skills/openspec-explore/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
---
|
||||
name: openspec-explore
|
||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Handling Different Entry Points
|
||||
|
||||
**User brings a vague idea:**
|
||||
```
|
||||
User: I'm thinking about adding real-time collaboration
|
||||
|
||||
You: Real-time collab is a big space. Let me think about this...
|
||||
|
||||
COLLABORATION SPECTRUM
|
||||
════════════════════════════════════════════
|
||||
|
||||
Awareness Coordination Sync
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│Presence│ │Cursors │ │ CRDT │
|
||||
│ "3 │ │ Multi │ │Conflict│
|
||||
│online" │ │ select │ │ free │
|
||||
└────────┘ └────────┘ └────────┘
|
||||
│ │ │
|
||||
trivial moderate complex
|
||||
|
||||
Where's your head at?
|
||||
```
|
||||
|
||||
**User brings a specific problem:**
|
||||
```
|
||||
User: The auth system is a mess
|
||||
|
||||
You: [reads codebase]
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CURRENT AUTH FLOW │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Google │ │ GitHub │ │ Email │
|
||||
│ OAuth │ │ OAuth │ │ Magic │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
└─────────────┼─────────────┘
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Session │
|
||||
└─────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Perms │
|
||||
└───────────┘
|
||||
|
||||
I see three tangles. Which one's burning?
|
||||
```
|
||||
|
||||
**User is stuck mid-implementation:**
|
||||
```
|
||||
User: /opsx:explore add-auth-system
|
||||
The OAuth integration is more complex than expected
|
||||
|
||||
You: [reads change artifacts]
|
||||
|
||||
You're on task 4: "Implement OAuth flow"
|
||||
|
||||
Let me trace what's involved...
|
||||
|
||||
[draws diagram, explores options, suggests paths]
|
||||
|
||||
Want to update the design to reflect this?
|
||||
Or add a spike task to investigate?
|
||||
```
|
||||
|
||||
**User wants to compare options:**
|
||||
```
|
||||
User: Should we use Postgres or SQLite?
|
||||
|
||||
You: Generic answer is boring. What's the context?
|
||||
|
||||
User: A CLI tool that tracks local dev environments
|
||||
|
||||
You: That changes everything.
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Key constraints:
|
||||
• No daemon running
|
||||
• Must work offline
|
||||
• Single user
|
||||
|
||||
SQLite Postgres
|
||||
Deployment embedded ✓ needs server ✗
|
||||
Offline yes ✓ no ✗
|
||||
Single file yes ✓ no ✗
|
||||
|
||||
SQLite. Not even close.
|
||||
|
||||
Unless... is there a sync component?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When it feels like things are crystallizing, you might summarize:
|
||||
|
||||
```
|
||||
## What We Figured Out
|
||||
|
||||
**The problem**: [crystallized understanding]
|
||||
|
||||
**The approach**: [if one emerged]
|
||||
|
||||
**Open questions**: [if any remain]
|
||||
|
||||
**Next steps** (if ready):
|
||||
- Create a change proposal
|
||||
- Keep exploring: just keep talking
|
||||
```
|
||||
|
||||
But this summary is optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
110
.github/skills/openspec-propose/SKILL.md
vendored
Normal file
110
.github/skills/openspec-propose/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
name: openspec-propose
|
||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
29
.github/skills/python.md
vendored
Normal file
29
.github/skills/python.md
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Skill: Python (Odoo ORM)
|
||||
|
||||
## Estilo y convenciones
|
||||
- Sigue las OCA guidelines estrictamente.
|
||||
- Usa `black` para formateo automático (ver `pyproject.toml`).
|
||||
- Ordena imports con `isort` (perfil black).
|
||||
- Linting obligatorio con `flake8` y `pylint-odoo`.
|
||||
- Usa docstrings en clases y métodos públicos.
|
||||
- Comentarios inline solo para lógica compleja.
|
||||
|
||||
## Patrones clave
|
||||
- Modelos: hereda de `models.Model` o `models.TransientModel`.
|
||||
- Campos de negocio siempre en `product.product` (no en `product.template`).
|
||||
- Usa `@api.depends` para campos computados.
|
||||
- Notificaciones: usa `ir.actions.client` con `display_notification`.
|
||||
- Logging: `_logger.info()` para trazabilidad de precios y descuentos.
|
||||
- Bulk updates: prefiere `search().write()` sobre loops.
|
||||
|
||||
## Testing
|
||||
- Tests en `tests/` de cada addon.
|
||||
- Hereda de `odoo.tests.common.TransactionCase`.
|
||||
- Ejecuta tests con:
|
||||
```bash
|
||||
docker-compose run odoo odoo -d odoo --test-enable --stop-after-init -u addon_name
|
||||
```
|
||||
|
||||
## Errores comunes
|
||||
- No uses `_()` en definiciones de campos (solo en métodos ejecutables).
|
||||
- Usa `related` fields en templates para lógica de variantes.
|
||||
18
.github/skills/xml.md
vendored
Normal file
18
.github/skills/xml.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Skill: XML (Odoo Views)
|
||||
|
||||
## Estilo y convenciones
|
||||
- Indentación: 4 espacios (no tabs).
|
||||
- Usa `position` explícito en XPaths (`before`, `after`, `inside`, `replace`, `attributes`).
|
||||
- Referencia grupos con `module.xml_id`.
|
||||
- Usa `sequence` para ordenar campos en vistas.
|
||||
- No pongas lógica condicional en QWeb, solo acceso a atributos.
|
||||
|
||||
## Patrones clave
|
||||
- Define vistas en `views/*.xml`.
|
||||
- IDs: usa el patrón `modulo.nombre_descriptivo`.
|
||||
- Traduce strings en XML, nunca en Python para campos.
|
||||
- Para QWeb, prepara todos los datos en el controller.
|
||||
|
||||
## Errores comunes
|
||||
- XPath not found: revisa la herencia y los XML IDs.
|
||||
- No uses lógica compleja en QWeb (ver QWEB_BEST_PRACTICES.md).
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -131,3 +131,8 @@ dmypy.json
|
|||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
tmp/
|
||||
.claude/
|
||||
.vscode/
|
||||
.github
|
||||
CLAUDE.md
|
||||
|
|
|
|||
16
.isort.cfg
Normal file
16
.isort.cfg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[isort]
|
||||
profile = black
|
||||
force_single_line = True
|
||||
# For black compatibility
|
||||
line_length = 88
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = True
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = True
|
||||
ensure_newline_before_comments = True
|
||||
# Skip __init__.py as per OCA standards
|
||||
skip_glob = */__init__.py
|
||||
known_odoo=odoo
|
||||
known_odoo_addons=odoo.addons
|
||||
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
|
||||
default_section=THIRDPARTY
|
||||
137
.pre-commit-config.yaml
Normal file
137
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
exclude: |
|
||||
(?x)
|
||||
# NOT INSTALLABLE ADDONS
|
||||
# END NOT INSTALLABLE ADDONS
|
||||
# Files and folders generated by bots, to avoid loops
|
||||
/setup/|/README\.rst$|/static/description/index\.html$|
|
||||
# Maybe reactivate this when all README files include prettier ignore tags?
|
||||
^README\.md$|
|
||||
# Library files can have extraneous formatting (even minimized)
|
||||
/static/(src/)?lib/|
|
||||
# Repos using Sphinx to generate docs don't need prettying
|
||||
^docs/_templates/.*\.html$|
|
||||
# You don't usually want a bot to modify your legal texts
|
||||
(LICENSE.*|COPYING.*)
|
||||
default_language_version:
|
||||
python: python3
|
||||
node: "16.17.0"
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
# These files are most likely copier diff rejection junks; if found,
|
||||
# review them manually, fix the problem (if needed) and remove them
|
||||
- id: forbidden-files
|
||||
name: forbidden files
|
||||
entry: found forbidden files; remove them
|
||||
language: fail
|
||||
files: "\\.rej$"
|
||||
- repo: https://github.com/oca/maintainer-tools
|
||||
rev: 71aa4caec15e8c1456b4da19e9f39aa0aa7377a9
|
||||
hooks:
|
||||
# update the NOT INSTALLABLE ADDONS section above
|
||||
- id: oca-update-pre-commit-excluded-addons
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v2.3.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: ["-i", "--ignore-init-module-imports"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 26.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier + plugin-xml
|
||||
additional_dependencies:
|
||||
- "prettier@2.7.1"
|
||||
- "@prettier/plugin-xml@2.2.0"
|
||||
args:
|
||||
- --plugin=@prettier/plugin-xml
|
||||
files: \.(css|htm|html|js|json|json5|scss|toml|xml|yaml|yml)$
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v10.0.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
verbose: true
|
||||
args:
|
||||
- --color
|
||||
- --fix
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
# exclude autogenerated files, and patches: the blank context
|
||||
# lines of a diff are a single space, stripping it breaks them
|
||||
exclude: /README\.rst$|\.pot?$|\.patch$
|
||||
- id: end-of-file-fixer
|
||||
# exclude autogenerated files, and patches: they are kept as
|
||||
# git format-patch wrote them
|
||||
exclude: /README\.rst$|\.pot?$|\.patch$
|
||||
- id: debug-statements
|
||||
- id: check-case-conflict
|
||||
- id: check-docstring-first
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
# exclude files where underlines are not distinguishable from merge conflicts
|
||||
exclude: /README\.rst$|^docs/.*\.rst$
|
||||
- id: check-symlinks
|
||||
- id: check-xml
|
||||
- id: mixed-line-ending
|
||||
args: ["--fix=lf"]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py38-plus"]
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort except __init__.py
|
||||
args:
|
||||
- --settings=.
|
||||
exclude: /__init__\.py$
|
||||
# setuptools-odoo deshabilitado temporalmente (no soporta Odoo 18.0)
|
||||
# - repo: https://github.com/acsone/setuptools-odoo
|
||||
# rev: 3.3.2
|
||||
# hooks:
|
||||
# - id: setuptools-odoo-make-default
|
||||
# - id: setuptools-odoo-get-requirements
|
||||
# args:
|
||||
# - --output
|
||||
# - requirements.txt
|
||||
# - --header
|
||||
# - "# generated from manifests external_dependencies"
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
name: flake8
|
||||
additional_dependencies: ["flake8-bugbear==23.12.2"]
|
||||
# NOTE: mypy is intentionally not configured here. It is not part of the
|
||||
# OCA pre-commit standard and breaks on Odoo's migration folder layout
|
||||
# (e.g. ".../migrations/18.0.2.1.0/post-migrate.py" -> not a valid package
|
||||
# name), because mypy's exclude does not apply to files passed explicitly
|
||||
# by pre-commit.
|
||||
- repo: https://github.com/PyCQA/pylint
|
||||
rev: v4.0.4
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint with optional checks
|
||||
args:
|
||||
- --rcfile=.pylintrc
|
||||
- --exit-zero
|
||||
verbose: true
|
||||
exclude: ^scripts/
|
||||
additional_dependencies: &pylint_deps
|
||||
- pylint-odoo==10.0.0
|
||||
- id: pylint
|
||||
name: pylint with mandatory checks
|
||||
args:
|
||||
- --rcfile=.pylintrc-mandatory
|
||||
exclude: ^scripts/
|
||||
additional_dependencies: *pylint_deps
|
||||
15
.prettierignore
Normal file
15
.prettierignore
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Prettier ignore patterns for Odoo addons
|
||||
|
||||
# Ignore XML files - prettier has issues with QWeb mixed content
|
||||
*.xml
|
||||
|
||||
# Odoo core
|
||||
ocb/**
|
||||
|
||||
# Build artifacts
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
|
||||
# Git
|
||||
.git/
|
||||
30
.prettierrc.yml
Normal file
30
.prettierrc.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Prettier configuration for Odoo addons
|
||||
# Note: XML formatting disabled for QWeb templates due to prettier limitations
|
||||
# with mixed content (text + tags). Use manual formatting for .xml files.
|
||||
|
||||
printWidth: 100
|
||||
tabWidth: 4
|
||||
useTabs: false
|
||||
|
||||
# XML/HTML specific - disabled, causes readability issues with QWeb
|
||||
# xmlWhitespaceSensitivity: "strict"
|
||||
# xmlSelfClosingSpace: true
|
||||
|
||||
# Keep tags more compact - don't break every attribute
|
||||
overrides:
|
||||
# XML files with improved readability
|
||||
- files: "*.xml"
|
||||
options:
|
||||
printWidth: 100
|
||||
xmlWhitespaceSensitivity: "ignore"
|
||||
singleAttributePerLine: true
|
||||
bracketSameLine: false
|
||||
|
||||
- files: "*.py"
|
||||
options:
|
||||
printWidth: 88
|
||||
|
||||
- files: ["*.json", "*.json5"]
|
||||
options:
|
||||
printWidth: 120
|
||||
tabWidth: 2
|
||||
91
.pylintrc
Normal file
91
.pylintrc
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
[MASTER]
|
||||
load-plugins=pylint_odoo
|
||||
score=n
|
||||
# Exclude virtual environment directory from pylint analysis
|
||||
ignore=.venv
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=all
|
||||
|
||||
# This .pylintrc contains optional AND mandatory checks and is meant to be
|
||||
# loaded in an IDE to have it check everything, in the hope this will make
|
||||
# optional checks more visible to contributors who otherwise never look at a
|
||||
# green CI pipeline.
|
||||
#
|
||||
# .pylintrc-mandatory will be used by pre-commit to check only mandatory
|
||||
# checks.
|
||||
|
||||
enable=anomalous-backslash-in-string,
|
||||
assignment-from-none,
|
||||
attribute-deprecated,
|
||||
attribute-string-redundant,
|
||||
bad-builtin-groupby,
|
||||
category-allowed,
|
||||
category-allowed-app,
|
||||
consider-merging-classes-inherited,
|
||||
context-overridden,
|
||||
dangerous-default-value,
|
||||
deprecated-name-get,
|
||||
deprecated-odoo-model-method,
|
||||
development-status-allowed,
|
||||
duplicate-key,
|
||||
except-pass,
|
||||
external-request-timeout,
|
||||
inheritable-method-lambda,
|
||||
inheritable-method-string,
|
||||
invalid-commit,
|
||||
invalid-email,
|
||||
license-allowed,
|
||||
manifest-author-string,
|
||||
manifest-behind-migrations,
|
||||
manifest-data-duplicated,
|
||||
manifest-deprecated-key,
|
||||
manifest-external-assets,
|
||||
manifest-maintainers-list,
|
||||
manifest-required-key,
|
||||
manifest-required-key-app,
|
||||
manifest-superfluous-key,
|
||||
manifest-version-format,
|
||||
method-compute,
|
||||
method-inverse,
|
||||
method-required-super,
|
||||
method-search,
|
||||
missing-odoo-file,
|
||||
missing-odoo-file-app,
|
||||
missing-return,
|
||||
no-raise-unlink,
|
||||
no-search-all,
|
||||
no-wizard-in-models,
|
||||
no-write-in-compute,
|
||||
odoo-addons-relative-import,
|
||||
odoo-exception-warning,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
prefer-env-translation,
|
||||
print-used,
|
||||
prohibited-method-override,
|
||||
redundant-keyword-arg,
|
||||
renamed-field-parameter,
|
||||
resource-not-exist,
|
||||
sql-injection,
|
||||
super-method-mismatch,
|
||||
test-folder-imported,
|
||||
too-few-format-args,
|
||||
translation-contains-variable,
|
||||
translation-field,
|
||||
translation-format-interpolation,
|
||||
translation-format-truncated,
|
||||
translation-fstring-interpolation,
|
||||
translation-not-lazy,
|
||||
translation-positional-used,
|
||||
translation-required,
|
||||
translation-too-few-args,
|
||||
translation-too-many-args,
|
||||
translation-unsupported-format,
|
||||
use-vim-comment,
|
||||
website-manifest-key-not-valid-uri
|
||||
|
||||
[REPORTS]
|
||||
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||
output-format=colorized
|
||||
reports=no
|
||||
59
.pylintrc-mandatory
Normal file
59
.pylintrc-mandatory
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
[MASTER]
|
||||
load-plugins=pylint_odoo
|
||||
score=n
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=all
|
||||
|
||||
enable=anomalous-backslash-in-string,
|
||||
assignment-from-none,
|
||||
attribute-deprecated,
|
||||
consider-merging-classes-inherited,
|
||||
context-overridden,
|
||||
dangerous-default-value,
|
||||
deprecated-name-get,
|
||||
deprecated-odoo-model-method,
|
||||
development-status-allowed,
|
||||
duplicate-key,
|
||||
except-pass,
|
||||
external-request-timeout,
|
||||
inheritable-method-lambda,
|
||||
inheritable-method-string,
|
||||
invalid-commit,
|
||||
license-allowed,
|
||||
manifest-author-string,
|
||||
manifest-behind-migrations,
|
||||
manifest-data-duplicated,
|
||||
manifest-deprecated-key,
|
||||
manifest-maintainers-list,
|
||||
manifest-required-key,
|
||||
manifest-superfluous-key,
|
||||
manifest-version-format,
|
||||
method-compute,
|
||||
method-inverse,
|
||||
method-required-super,
|
||||
method-search,
|
||||
missing-return,
|
||||
no-raise-unlink,
|
||||
no-write-in-compute,
|
||||
odoo-addons-relative-import,
|
||||
odoo-exception-warning,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
print-used,
|
||||
prohibited-method-override,
|
||||
redundant-keyword-arg,
|
||||
renamed-field-parameter,
|
||||
resource-not-exist,
|
||||
sql-injection,
|
||||
test-folder-imported,
|
||||
too-few-format-args,
|
||||
translation-field,
|
||||
translation-positional-used,
|
||||
translation-required,
|
||||
use-vim-comment
|
||||
|
||||
[REPORTS]
|
||||
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||
output-format=colorized
|
||||
reports=no
|
||||
12
.vscode/extensions.json
vendored
Normal file
12
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-python.python",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.pylint",
|
||||
"ms-python.flake8",
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"odoo.odoo-snippets"
|
||||
]
|
||||
}
|
||||
54
.vscode/settings.json
vendored
Normal file
54
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"xml.symbols.enabled": false,
|
||||
"python.languageServer": "None",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.pylintArgs": ["--rcfile=.pylintrc"],
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.flake8Args": ["--config=.flake8"],
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackArgs": ["--config=pyproject.toml"],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"python.sortImports.args": ["--settings-path=.isort.cfg"],
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"[xml]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/*.pyc": true,
|
||||
"**/*.pyo": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/.tox": true,
|
||||
"**/.eggs": true,
|
||||
"**/*.egg-info": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/*.pyc": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/.tox": true,
|
||||
"**/.eggs": true,
|
||||
"**/*.egg-info": true
|
||||
}
|
||||
}
|
||||
81
Makefile
Normal file
81
Makefile
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
.PHONY: help
|
||||
help:
|
||||
@echo "Comandos disponibles para verificación de código OCA:"
|
||||
@echo ""
|
||||
@echo " make lint - Ejecutar todas las verificaciones (pre-commit)"
|
||||
@echo " make format - Formatear código (black + isort)"
|
||||
@echo " make check-format - Verificar formateo sin modificar"
|
||||
@echo " make flake8 - Ejecutar flake8"
|
||||
@echo " make pylint - Ejecutar pylint (opcionales y mandatorios)"
|
||||
@echo " make pylint-required - Ejecutar solo verificaciones mandatorias"
|
||||
@echo " make test - Ejecutar tests"
|
||||
@echo " make install-hooks - Instalar pre-commit hooks"
|
||||
@echo " make update-hooks - Actualizar pre-commit hooks"
|
||||
@echo " make clean - Limpiar archivos temporales"
|
||||
@echo ""
|
||||
|
||||
.PHONY: install-hooks
|
||||
install-hooks:
|
||||
pre-commit install
|
||||
|
||||
.PHONY: update-hooks
|
||||
update-hooks:
|
||||
pre-commit autoupdate
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
pre-commit run --all-files
|
||||
|
||||
.PHONY: lint-changed
|
||||
lint-changed:
|
||||
pre-commit run
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
black .
|
||||
isort .
|
||||
|
||||
.PHONY: check-format
|
||||
check-format:
|
||||
black --check .
|
||||
isort --check-only .
|
||||
|
||||
.PHONY: flake8
|
||||
flake8:
|
||||
flake8 .
|
||||
|
||||
.PHONY: pylint
|
||||
pylint:
|
||||
@for addon in $$(ls -d */ | grep -v "\."); do \
|
||||
if [ -f "$$addon/__manifest__.py" ] || [ -f "$$addon/__openerp__.py" ]; then \
|
||||
echo "Checking $$addon with pylint..."; \
|
||||
pylint --rcfile=.pylintrc --exit-zero $$addon; \
|
||||
fi \
|
||||
done
|
||||
|
||||
.PHONY: pylint-required
|
||||
pylint-required:
|
||||
@for addon in $$(ls -d */ | grep -v "\."); do \
|
||||
if [ -f "$$addon/__manifest__.py" ] || [ -f "$$addon/__openerp__.py" ]; then \
|
||||
echo "Checking $$addon with pylint (mandatory checks)..."; \
|
||||
pylint --rcfile=.pylintrc-mandatory $$addon; \
|
||||
fi \
|
||||
done
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "Ejecutar tests de Odoo aquí"
|
||||
@echo "Por ejemplo: pytest o python -m pytest"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
find . -type f -name '*.pyc' -delete
|
||||
find . -type d -name '__pycache__' -delete
|
||||
find . -type d -name '*.egg-info' -exec rm -rf {} + || true
|
||||
find . -type d -name '.pytest_cache' -exec rm -rf {} + || true
|
||||
find . -type d -name '.tox' -exec rm -rf {} + || true
|
||||
rm -rf build/ dist/
|
||||
|
||||
.PHONY: requirements
|
||||
requirements:
|
||||
pre-commit run setuptools-odoo-get-requirements --all-files
|
||||
104
README.md
Normal file
104
README.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Odoo Addons — Criptomart
|
||||
|
||||
Colección de addons Odoo 18.0 para Criptomart / Elika Bilbo. Incluye addons OCA incluidos como dependencias y addons custom desarrollados internamente.
|
||||
|
||||
## Addons OCA (dependencias incluidas)
|
||||
|
||||
Addons del ecosistema [OCA](https://github.com/OCA) incorporados al repositorio. No se modifican salvo hotfixes puntuales.
|
||||
|
||||
| Addon | Versión | Descripción |
|
||||
| ----- | ------- | ----------- |
|
||||
| [account_invoice_triple_discount](account_invoice_triple_discount/) | 18.0.1.0.0 | Triple descuento en líneas de factura |
|
||||
| [product_get_price_helper](product_get_price_helper/) | 18.0.1.1.0 | Helper para obtener precios de producto respetando tarifas |
|
||||
| [product_main_seller](product_main_seller/) | 18.0.1.0.0 | Campo "proveedor principal" en producto |
|
||||
| [product_origin_char](product_origin_char/) | 18.0.2.0.0 | Campo de texto libre para origen del producto |
|
||||
| [product_price_category](product_price_category/) | 18.0.1.0.0 | Categoría de precio en producto + aplicación masiva vía tarifas |
|
||||
| [purchase_triple_discount](purchase_triple_discount/) | 18.0.1.0.0 | Triple descuento en líneas de pedido de compra |
|
||||
|
||||
## Addons Custom
|
||||
|
||||
Addons desarrollados por Criptomart para necesidades específicas del proyecto.
|
||||
|
||||
| Addon | Versión | Descripción |
|
||||
| ----- | ------- | ----------- |
|
||||
| [account_invoice_triple_discount_readonly](account_invoice_triple_discount_readonly/) | 18.0.1.0.0 | Corrige bug de acumulación de descuentos y pone descuento total en solo lectura |
|
||||
| [membership_expiry_reminder](membership_expiry_reminder/) | 18.0.1.0.0 | Recordatorio por email de membresías próximas a vencer |
|
||||
| [portal_event_registration](portal_event_registration/) | 18.0.1.0.0 | Vista portal de inscripciones a eventos con adjuntos al chatter |
|
||||
| [product_price_category_supplier](product_price_category_supplier/) | 18.0.1.0.0 | Categoría de precio por defecto en proveedor + actualización masiva de productos |
|
||||
| [product_pricelist_total_margin](product_pricelist_total_margin/) | 18.0.1.2.0 | Margen aditivo (no compuesto) en tarifas encadenadas, con límites globales |
|
||||
| [product_sale_price_from_pricelist](product_sale_price_from_pricelist/) | 18.0.2.7.0 | Calcula precio de venta desde último precio de compra vía tarifa configurable |
|
||||
| [stock_picking_batch_collect](stock_picking_batch_collect/) | 18.0.2.0.0 | Montaje de cestas en lotes: columnas extra, resumen por producto y vista operario |
|
||||
| [web_list_striped](web_list_striped/) | 18.0.1.0.0 | Efecto cebra y hover suave en todas las listas del backend (sólo CSS) |
|
||||
| [website_sale_aplicoop](website_sale_aplicoop/) | 18.0.1.17.0 | Sistema de pedidos colaborativos para grupos de consumo (reemplazo de Aplicoop) |
|
||||
|
||||
## Dependencias entre addons custom
|
||||
|
||||
```text
|
||||
website_sale_aplicoop
|
||||
└── product_sale_price_from_pricelist
|
||||
└── product_pricelist_total_margin
|
||||
└── product_price_category
|
||||
└── product_main_seller
|
||||
└── product_price_category_supplier
|
||||
└── product_price_category
|
||||
└── stock_picking_batch_collect
|
||||
└── stock_move_manual_quantity
|
||||
|
||||
stock_picking_batch_collect
|
||||
└── stock_move_manual_quantity
|
||||
└── stock_picking_batch (Odoo core)
|
||||
|
||||
account_invoice_triple_discount_readonly
|
||||
└── account_invoice_triple_discount
|
||||
└── purchase_triple_discount
|
||||
```
|
||||
|
||||
## Desarrollo
|
||||
|
||||
### Instalación / actualización de un addon
|
||||
|
||||
```bash
|
||||
docker-compose exec -T odoo odoo -d odoo -u <nombre_addon> --stop-after-init
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
docker-compose exec -T odoo odoo -d odoo --test-enable --stop-after-init -u <nombre_addon>
|
||||
```
|
||||
|
||||
### Linters
|
||||
|
||||
```bash
|
||||
# Python
|
||||
black . && isort . && flake8 . && pylint --load-plugins=pylint_odoo <addon>/
|
||||
|
||||
# JS
|
||||
npx eslint <addon>/static/src/
|
||||
```
|
||||
|
||||
Ver [docs/LINTERS_README.md](docs/LINTERS_README.md) para configuración completa.
|
||||
|
||||
### Traducciones
|
||||
|
||||
Ver [docs/TRANSLATIONS.md](docs/TRANSLATIONS.md).
|
||||
|
||||
## CI/CD (Forgejo Actions)
|
||||
|
||||
En `.gitea/workflows/`, sobre un runner (etiqueta `issue-bot`) registrado en la organización
|
||||
CriptoMart en la misma máquina e imagen que el de ecocentral; ver
|
||||
[tools/issue_bot/README.md](tools/issue_bot/README.md#con-actions):
|
||||
|
||||
| Workflow | Cuándo | Qué hace |
|
||||
| -------- | ------ | -------- |
|
||||
| `ci.yml` | PR y push a `18.0` | `lint` (pre-commit), `test` ([tools/ci/run_tests.py](tools/ci/run_tests.py)) y `review` (bot de revisión, tras pasar los dos) |
|
||||
| `issue-bot.yml` | Incidencia abierta o comentada | [Bot de incidencias](tools/issue_bot/README.md): responde o abre un PR en `bot/issue-N` |
|
||||
| `pr-bot.yml` | Se quita `WIP` o se pone la etiqueta `i18n` | [Bot de revisión](tools/pr_bot/README.md), si el CI ya había pasado |
|
||||
|
||||
El contexto que reciben los bots sobre este repo está en [.gitea/issue-bot.md](.gitea/issue-bot.md).
|
||||
Los repos OCA de los que dependen los tests y la imagen del runner se mantienen en el repo de
|
||||
ecocentral (`tools/issue_bot/runner/`).
|
||||
|
||||
## Documentación técnica
|
||||
|
||||
Ver carpeta [docs/](docs/) para documentación transversal (instalación, lazy loading, QWeb, etc.).
|
||||
31
account_banking_mandate_batch/README.md
Normal file
31
account_banking_mandate_batch/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Account Banking Mandate Batch Create
|
||||
|
||||
Adds a contextual server action **Create SEPA mandates** to the Contacts
|
||||
(`res.partner`) list and form views.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Go to *Contacts* (or *Accounting → Customers*), switch to list view.
|
||||
2. Filter the partners you want (e.g. your members: `is_client = True`).
|
||||
3. Select them (tick the header checkbox to select all).
|
||||
4. *Actions → Create SEPA mandates*.
|
||||
|
||||
For each selected partner the action:
|
||||
|
||||
- takes the partner's first bank account (`bank_ids[:1]`),
|
||||
- creates a recurrent SEPA Core mandate (`format=sepa`, `type=recurrent`,
|
||||
`scheme=CORE`) with today's signature date,
|
||||
- validates it (state → *valid*).
|
||||
|
||||
Partners with no bank account, or that already have a draft/valid mandate,
|
||||
are skipped. A notification reports created / skipped counts.
|
||||
|
||||
> **Legal note:** only create mandates for members who have actually
|
||||
> authorised the direct debit. Adjust the signature date in
|
||||
> `data/server_action.xml` if you need the real authorisation date.
|
||||
|
||||
## Requirements
|
||||
|
||||
Depends on `account_banking_sepa_direct_debit` (OCA `bank-payment`), which
|
||||
pulls in `account_banking_mandate`, `account_payment_order` and
|
||||
`account_payment_mode`.
|
||||
14
account_banking_mandate_batch/__manifest__.py
Normal file
14
account_banking_mandate_batch/__manifest__.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "Account Banking Mandate Batch Create",
|
||||
"summary": "Adds a contextual action on contacts to create and validate "
|
||||
"SEPA direct debit mandates in bulk for the selected partners.",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "AGPL-3",
|
||||
"author": "Criptomart",
|
||||
"depends": ["account_banking_sepa_direct_debit"],
|
||||
"data": [
|
||||
"data/server_action.xml",
|
||||
],
|
||||
}
|
||||
60
account_banking_mandate_batch/data/server_action.xml
Normal file
60
account_banking_mandate_batch/data/server_action.xml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="action_create_sepa_mandates" model="ir.actions.server">
|
||||
<field name="name">Create SEPA mandates</field>
|
||||
<field name="model_id" ref="base.model_res_partner" />
|
||||
<field name="binding_model_id" ref="base.model_res_partner" />
|
||||
<field name="binding_view_types">list,form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code"><![CDATA[
|
||||
# Bulk-create and validate recurrent SEPA Core mandates for the selected
|
||||
# partners. Filter the contacts list (e.g. is_client = True), select all,
|
||||
# then run this action. One mandate per partner; partners that already have
|
||||
# a draft/valid mandate or no bank account are skipped.
|
||||
Mandate = env["account.banking.mandate"]
|
||||
today = datetime.date.today()
|
||||
|
||||
created = 0
|
||||
no_bank = 0
|
||||
already = 0
|
||||
|
||||
for partner in records:
|
||||
bank = partner.bank_ids[:1]
|
||||
if not bank:
|
||||
no_bank += 1
|
||||
continue
|
||||
if Mandate.search_count([
|
||||
("partner_id", "=", partner.id),
|
||||
("state", "in", ("draft", "valid")),
|
||||
]):
|
||||
already += 1
|
||||
continue
|
||||
mandate = Mandate.create({
|
||||
"partner_bank_id": bank.id,
|
||||
"format": "sepa",
|
||||
"type": "recurrent",
|
||||
"scheme": "CORE",
|
||||
"signature_date": today,
|
||||
})
|
||||
mandate.validate()
|
||||
created += 1
|
||||
|
||||
message = (
|
||||
"SEPA mandates created: %s\n"
|
||||
"Already had a mandate: %s\n"
|
||||
"Skipped (no bank account): %s"
|
||||
) % (created, already, no_bank)
|
||||
|
||||
action = {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": "SEPA mandates",
|
||||
"message": message,
|
||||
"type": "success" if created else "warning",
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
165
account_invoice_triple_discount_readonly/README.rst
Normal file
165
account_invoice_triple_discount_readonly/README.rst
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
==========================================
|
||||
Account Invoice Triple Discount Readonly
|
||||
==========================================
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
|badge1| |badge2|
|
||||
|
||||
This module fixes a bug in ``account_invoice_triple_discount`` when combined with ``purchase_triple_discount``.
|
||||
|
||||
**Problem Solved:**
|
||||
|
||||
In purchase orders, when configuring discounts in the ``discount2`` or ``discount3`` columns:
|
||||
|
||||
* Upon saving, all discounts were accumulated in the ``discount1`` column
|
||||
* The ``discount2`` and ``discount3`` fields were reset to zero
|
||||
* This occurred because the original mixin's ``write`` method always reset these fields
|
||||
|
||||
**Solution:**
|
||||
|
||||
The module overrides the ``write`` method of ``triple.discount.mixin`` to:
|
||||
|
||||
#. Detect if ``discount1``, ``discount2`` or ``discount3`` are being explicitly modified
|
||||
#. If there are explicit discounts, ignore the computed ``discount`` field
|
||||
#. If only the ``discount`` field comes, maintain legacy behavior
|
||||
#. Make the total ``discount`` field readonly in relevant views
|
||||
|
||||
**Additional Features:**
|
||||
|
||||
* Makes the ``discount`` field (total discount) **readonly** in:
|
||||
|
||||
* Partner form view (``res.partner``)
|
||||
* Supplier info in product form (``product.supplierinfo``)
|
||||
* Purchase order lines (``purchase.order.line``)
|
||||
* Account move lines (``account.move.line``)
|
||||
* Base mixin (``triple.discount.mixin``)
|
||||
|
||||
* Forces users to work only with the ``discount1``, ``discount2`` and ``discount3`` columns
|
||||
* Avoids confusion between computed total and individual discounts
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To install this module, you need to:
|
||||
|
||||
#. Ensure dependencies are installed:
|
||||
|
||||
* account_invoice_triple_discount (OCA)
|
||||
* purchase_triple_discount (OCA)
|
||||
|
||||
#. Update the Apps list
|
||||
#. Search for "Account Invoice Triple Discount Readonly"
|
||||
#. Click Install
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
No additional configuration is required after installation. The module automatically:
|
||||
|
||||
* Makes the total discount field readonly
|
||||
* Preserves individual discount values on save
|
||||
* Maintains backward compatibility with existing discounts
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
After installation:
|
||||
|
||||
**Working with Discounts:**
|
||||
|
||||
#. The total discount field will be readonly and automatically calculated
|
||||
#. Always use ``discount1``, ``discount2``, and ``discount3`` fields to set discounts
|
||||
#. When saving, the individual discount values will be preserved correctly
|
||||
|
||||
**Example:**
|
||||
|
||||
::
|
||||
|
||||
Product: Widget
|
||||
Price: 100.00€
|
||||
Discount 1: 10% → 90.00€
|
||||
Discount 2: 5% → 85.50€
|
||||
Discount 3: 2% → 83.79€
|
||||
|
||||
Total discount: 16.21% (displayed, readonly)
|
||||
|
||||
**Before this module:**
|
||||
|
||||
After saving, you would see:
|
||||
|
||||
::
|
||||
|
||||
Discount 1: 16.21% ❌ (accumulated)
|
||||
Discount 2: 0% ❌ (reset)
|
||||
Discount 3: 0% ❌ (reset)
|
||||
|
||||
**After this module:**
|
||||
|
||||
After saving, you correctly see:
|
||||
|
||||
::
|
||||
|
||||
Discount 1: 10% ✅
|
||||
Discount 2: 5% ✅
|
||||
Discount 3: 2% ✅
|
||||
Total: 16.21% (readonly, calculated)
|
||||
|
||||
**Important:**
|
||||
|
||||
* Do not try to edit the total discount field (it's readonly)
|
||||
* Always work with the three individual discount columns
|
||||
* The total is recalculated automatically
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/Criptomart/addons-cm/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Criptomart
|
||||
|
||||
Funding
|
||||
~~~~~~~
|
||||
|
||||
* Elika Bilbo
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* `Criptomart <https://criptomart.net>`_:
|
||||
|
||||
* Bug fix and implementation
|
||||
* Override logic for write method
|
||||
|
||||
Other credits
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This module fixes a critical bug in the interaction between OCA's ``account_invoice_triple_discount`` and ``purchase_triple_discount`` modules, ensuring discount values are preserved correctly across all document types.
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by Criptomart.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is part of the `Criptomart/addons-cm <https://github.com/Criptomart/addons-cm/tree/18.0/account_invoice_triple_discount_readonly>`_ project on GitHub.
|
||||
|
|
@ -1,18 +1,12 @@
|
|||
# Copyright 2025
|
||||
# Copyright 2025 - Today Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
{ # noqa: B018
|
||||
"name": "Account Invoice Triple Discount Readonly",
|
||||
"version": "16.0.1.0.0",
|
||||
"version": "18.0.1.0.0",
|
||||
"summary": "Make total discount readonly and fix discount2/discount3 write issue",
|
||||
"license": "AGPL-3",
|
||||
"author": "Criptomart",
|
||||
"website": "https://github.com/OCA/account-invoicing",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
"maintainers": ["Criptomart"],
|
||||
"website": "https://git.criptomart.net/criptomart/addons-cm",
|
||||
"depends": ["account_invoice_triple_discount", "purchase_triple_discount"],
|
||||
"data": [
|
||||
"views/product_supplierinfo_view.xml",
|
||||
"views/res_partner_view.xml",
|
||||
"views/purchase_order_view.xml",
|
||||
"views/account_move_view.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from odoo import fields, models
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class TripleDiscountMixin(models.AbstractModel):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# OCA Dependencies for account_invoice_triple_discount_readonly
|
||||
# Format: repository_name branch
|
||||
|
||||
account-invoicing https://github.com/OCA/account-invoicing.git 18.0
|
||||
purchase-workflow https://github.com/OCA/purchase-workflow.git 18.0
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
No additional configuration is required after installation. The module automatically:
|
||||
|
||||
* Makes the total discount field readonly
|
||||
* Preserves individual discount values on save
|
||||
* Maintains backward compatibility with existing discounts
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
* `Criptomart <https://criptomart.net>`_:
|
||||
|
||||
* Bug fix and implementation
|
||||
* Override logic for write method
|
||||
11
account_invoice_triple_discount_readonly/readme/CREDITS.rst
Normal file
11
account_invoice_triple_discount_readonly/readme/CREDITS.rst
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
**Authors:**
|
||||
|
||||
* Criptomart
|
||||
|
||||
**Funding:**
|
||||
|
||||
* Elika Bilbo
|
||||
|
||||
**Other credits:**
|
||||
|
||||
This module fixes a critical bug in the interaction between OCA's ``account_invoice_triple_discount`` and ``purchase_triple_discount`` modules, ensuring discount values are preserved correctly across all document types.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
This module fixes a bug in ``account_invoice_triple_discount`` when combined with ``purchase_triple_discount``.
|
||||
|
||||
**Problem Solved:**
|
||||
|
||||
In purchase orders, when configuring discounts in the ``discount2`` or ``discount3`` columns:
|
||||
|
||||
* Upon saving, all discounts were accumulated in the ``discount1`` column
|
||||
* The ``discount2`` and ``discount3`` fields were reset to zero
|
||||
* This occurred because the original mixin's ``write`` method always reset these fields
|
||||
|
||||
**Solution:**
|
||||
|
||||
The module overrides the ``write`` method of ``triple.discount.mixin`` to:
|
||||
|
||||
#. Detect if ``discount1``, ``discount2`` or ``discount3`` are being explicitly modified
|
||||
#. If there are explicit discounts, ignore the computed ``discount`` field
|
||||
#. If only the ``discount`` field comes, maintain legacy behavior
|
||||
#. Make the total ``discount`` field readonly in relevant views
|
||||
|
||||
**Additional Features:**
|
||||
|
||||
* Makes the ``discount`` field (total discount) **readonly** in:
|
||||
|
||||
* Partner form view (``res.partner``)
|
||||
* Supplier info in product form (``product.supplierinfo``)
|
||||
* Purchase order lines (``purchase.order.line``)
|
||||
* Account move lines (``account.move.line``)
|
||||
* Base mixin (``triple.discount.mixin``)
|
||||
|
||||
* Forces users to work only with the ``discount1``, ``discount2`` and ``discount3`` columns
|
||||
* Avoids confusion between computed total and individual discounts
|
||||
10
account_invoice_triple_discount_readonly/readme/INSTALL.rst
Normal file
10
account_invoice_triple_discount_readonly/readme/INSTALL.rst
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
To install this module, you need to:
|
||||
|
||||
#. Ensure dependencies are installed:
|
||||
|
||||
* account_invoice_triple_discount (OCA)
|
||||
* purchase_triple_discount (OCA)
|
||||
|
||||
#. Update the Apps list
|
||||
#. Search for "Account Invoice Triple Discount Readonly"
|
||||
#. Click Install
|
||||
46
account_invoice_triple_discount_readonly/readme/USAGE.rst
Normal file
46
account_invoice_triple_discount_readonly/readme/USAGE.rst
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
After installation:
|
||||
|
||||
**Working with Discounts:**
|
||||
|
||||
#. The total discount field will be readonly and automatically calculated
|
||||
#. Always use ``discount1``, ``discount2``, and ``discount3`` fields to set discounts
|
||||
#. When saving, the individual discount values will be preserved correctly
|
||||
|
||||
**Example:**
|
||||
|
||||
::
|
||||
|
||||
Product: Widget
|
||||
Price: 100.00€
|
||||
Discount 1: 10% → 90.00€
|
||||
Discount 2: 5% → 85.50€
|
||||
Discount 3: 2% → 83.79€
|
||||
|
||||
Total discount: 16.21% (displayed, readonly)
|
||||
|
||||
**Before this module:**
|
||||
|
||||
After saving, you would see:
|
||||
|
||||
::
|
||||
|
||||
Discount 1: 16.21% ❌ (accumulated)
|
||||
Discount 2: 0% ❌ (reset)
|
||||
Discount 3: 0% ❌ (reset)
|
||||
|
||||
**After this module:**
|
||||
|
||||
After saving, you correctly see:
|
||||
|
||||
::
|
||||
|
||||
Discount 1: 10% ✅
|
||||
Discount 2: 5% ✅
|
||||
Discount 3: 2% ✅
|
||||
Total: 16.21% (readonly, calculated)
|
||||
|
||||
**Important:**
|
||||
|
||||
* Do not try to edit the total discount field (it's readonly)
|
||||
* Always work with the three individual discount columns
|
||||
* The total is recalculated automatically
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
from . import test_triple_discount_mixin
|
||||
from . import test_account_move
|
||||
from . import test_purchase_order
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
# Copyright (C) 2025: Criptomart (https://criptomart.net)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAccountMove(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create a partner
|
||||
cls.partner = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Customer",
|
||||
"email": "customer@test.com",
|
||||
}
|
||||
)
|
||||
|
||||
# Create a product
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product Invoice",
|
||||
"type": "consu",
|
||||
"list_price": 200.0,
|
||||
"standard_price": 100.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Create tax
|
||||
cls.tax = cls.env["account.tax"].create(
|
||||
{
|
||||
"name": "Test Tax 10%",
|
||||
"amount": 10.0,
|
||||
"amount_type": "percent",
|
||||
"type_tax_use": "sale",
|
||||
}
|
||||
)
|
||||
|
||||
# Create an invoice
|
||||
cls.invoice = cls.env["account.move"].create(
|
||||
{
|
||||
"move_type": "out_invoice",
|
||||
"partner_id": cls.partner.id,
|
||||
"invoice_date": "2026-01-01",
|
||||
}
|
||||
)
|
||||
|
||||
# Create invoice line
|
||||
cls.invoice_line = cls.env["account.move.line"].create(
|
||||
{
|
||||
"move_id": cls.invoice.id,
|
||||
"product_id": cls.product.id,
|
||||
"quantity": 5,
|
||||
"price_unit": 200.0,
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
"discount3": 2.0,
|
||||
"tax_ids": [(6, 0, [cls.tax.id])],
|
||||
}
|
||||
)
|
||||
|
||||
def test_invoice_line_discount_readonly(self):
|
||||
"""Test that discount field is readonly in invoice lines"""
|
||||
field = self.invoice_line._fields["discount"]
|
||||
self.assertTrue(
|
||||
field.readonly, "Discount field should be readonly in invoice lines"
|
||||
)
|
||||
|
||||
def test_invoice_line_write_with_explicit_discounts(self):
|
||||
"""Test writing invoice line with explicit discounts"""
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"discount": 30.0, # Should be ignored
|
||||
"discount1": 15.0,
|
||||
"discount2": 10.0,
|
||||
"discount3": 5.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(self.invoice_line.discount1, 15.0)
|
||||
self.assertEqual(self.invoice_line.discount2, 10.0)
|
||||
self.assertEqual(self.invoice_line.discount3, 5.0)
|
||||
|
||||
def test_invoice_line_legacy_discount(self):
|
||||
"""Test legacy discount behavior in invoice lines"""
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"discount": 20.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Should map to discount1 and reset others
|
||||
self.assertEqual(self.invoice_line.discount1, 20.0)
|
||||
self.assertEqual(self.invoice_line.discount2, 0.0)
|
||||
self.assertEqual(self.invoice_line.discount3, 0.0)
|
||||
|
||||
def test_invoice_line_price_calculation(self):
|
||||
"""Test that price subtotal is calculated correctly with triple discount"""
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
"discount3": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Base: 5 * 200 = 1000
|
||||
# After 10% discount: 900
|
||||
# After 5% discount: 855
|
||||
expected_subtotal = 5 * 200 * 0.9 * 0.95
|
||||
self.assertAlmostEqual(
|
||||
self.invoice_line.price_subtotal, expected_subtotal, places=2
|
||||
)
|
||||
|
||||
def test_multiple_invoice_lines(self):
|
||||
"""Test multiple invoice lines with different discounts"""
|
||||
line2 = self.env["account.move.line"].create(
|
||||
{
|
||||
"move_id": self.invoice.id,
|
||||
"product_id": self.product.id,
|
||||
"quantity": 3,
|
||||
"price_unit": 150.0,
|
||||
"discount1": 20.0,
|
||||
"discount2": 10.0,
|
||||
"discount3": 5.0,
|
||||
"tax_ids": [(6, 0, [self.tax.id])],
|
||||
}
|
||||
)
|
||||
|
||||
# Verify both lines have correct discounts
|
||||
self.assertEqual(self.invoice_line.discount1, 10.0)
|
||||
self.assertEqual(line2.discount1, 20.0)
|
||||
self.assertEqual(line2.discount2, 10.0)
|
||||
self.assertEqual(line2.discount3, 5.0)
|
||||
|
||||
def test_invoice_line_update_quantity(self):
|
||||
"""Test updating quantity doesn't affect discounts"""
|
||||
initial_discount1 = self.invoice_line.discount1
|
||||
initial_discount2 = self.invoice_line.discount2
|
||||
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"quantity": 10,
|
||||
}
|
||||
)
|
||||
|
||||
# Discounts should remain unchanged
|
||||
self.assertEqual(self.invoice_line.discount1, initial_discount1)
|
||||
self.assertEqual(self.invoice_line.discount2, initial_discount2)
|
||||
# Quantity should be updated
|
||||
self.assertEqual(self.invoice_line.quantity, 10)
|
||||
|
||||
def test_invoice_line_update_price(self):
|
||||
"""Test updating price doesn't affect discounts"""
|
||||
initial_discount1 = self.invoice_line.discount1
|
||||
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"price_unit": 250.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Discount should remain unchanged
|
||||
self.assertEqual(self.invoice_line.discount1, initial_discount1)
|
||||
# Price should be updated
|
||||
self.assertEqual(self.invoice_line.price_unit, 250.0)
|
||||
|
||||
def test_invoice_with_zero_discounts(self):
|
||||
"""Test invoice line with all zero discounts"""
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"discount1": 0.0,
|
||||
"discount2": 0.0,
|
||||
"discount3": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
# All discounts should be zero
|
||||
self.assertEqual(self.invoice_line.discount, 0.0)
|
||||
self.assertEqual(self.invoice_line.discount1, 0.0)
|
||||
self.assertEqual(self.invoice_line.discount2, 0.0)
|
||||
self.assertEqual(self.invoice_line.discount3, 0.0)
|
||||
|
||||
# Subtotal should be quantity * price
|
||||
expected = 5 * 200
|
||||
self.assertEqual(self.invoice_line.price_subtotal, expected)
|
||||
|
||||
def test_invoice_line_combined_operations(self):
|
||||
"""Test combined operations on invoice line"""
|
||||
# Update multiple fields at once
|
||||
self.invoice_line.write(
|
||||
{
|
||||
"quantity": 8,
|
||||
"price_unit": 180.0,
|
||||
"discount1": 12.0,
|
||||
"discount2": 6.0,
|
||||
"discount3": 0.0, # Reset discount3 explicitly
|
||||
}
|
||||
)
|
||||
|
||||
# All fields should be updated correctly
|
||||
self.assertEqual(self.invoice_line.quantity, 8)
|
||||
self.assertEqual(self.invoice_line.price_unit, 180.0)
|
||||
self.assertEqual(self.invoice_line.discount1, 12.0)
|
||||
self.assertEqual(self.invoice_line.discount2, 6.0)
|
||||
self.assertEqual(self.invoice_line.discount3, 0.0)
|
||||
|
||||
# Calculate expected subtotal: 8 * 180 * (1-0.12) * (1-0.06)
|
||||
expected = 8 * 180 * 0.88 * 0.94
|
||||
self.assertAlmostEqual(self.invoice_line.price_subtotal, expected, places=2)
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
# Copyright (C) 2025: Criptomart (https://criptomart.net)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestPurchaseOrder(TransactionCase):
|
||||
"""Purchase lines and the vendor bills created from them.
|
||||
|
||||
In 18.0 ``purchase.order.line`` does not use ``triple.discount.mixin`` but
|
||||
``purchase.triple.discount.mixin`` (purchase_triple_discount), where the
|
||||
total discount is a stored compute of discount1..3: the accumulation bug
|
||||
this addon fixes does not happen there. What matters on the purchase side
|
||||
is that the three discounts reach the bill untouched, and the bill lines do
|
||||
use the mixin this addon patches.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create a supplier
|
||||
cls.supplier = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Supplier",
|
||||
"email": "supplier@test.com",
|
||||
"supplier_rank": 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Create a product template first, then get the variant. Billed on
|
||||
# ordered quantities, so the bill can be created without a receipt.
|
||||
cls.product_template = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Test Product PO",
|
||||
"type": "consu",
|
||||
"list_price": 150.0,
|
||||
"standard_price": 80.0,
|
||||
"purchase_method": "purchase",
|
||||
}
|
||||
)
|
||||
# Get the auto-created product variant
|
||||
cls.product = cls.product_template.product_variant_ids[0]
|
||||
|
||||
# Create a purchase order
|
||||
cls.purchase_order = cls.env["purchase.order"].create(
|
||||
{
|
||||
"partner_id": cls.supplier.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Create purchase order line
|
||||
cls.po_line = cls.env["purchase.order.line"].create(
|
||||
{
|
||||
"order_id": cls.purchase_order.id,
|
||||
"product_id": cls.product.id,
|
||||
"product_qty": 10,
|
||||
"price_unit": 150.0,
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
"discount3": 2.0,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_bill_line(self):
|
||||
self.purchase_order.button_confirm()
|
||||
self.purchase_order.action_create_invoice()
|
||||
return self.purchase_order.invoice_ids.invoice_line_ids.filtered(
|
||||
lambda line: line.product_id == self.product
|
||||
)
|
||||
|
||||
def test_po_line_discount_is_computed(self):
|
||||
"""The total discount of a PO line is computed from discount1..3"""
|
||||
field = self.po_line._fields["discount"]
|
||||
self.assertTrue(field.compute)
|
||||
expected = 100 - (100 * 0.90 * 0.95 * 0.98)
|
||||
self.assertAlmostEqual(self.po_line.discount, expected, places=2)
|
||||
|
||||
def test_po_line_write_with_explicit_discounts(self):
|
||||
"""Test writing PO line with explicit discounts"""
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 12.0,
|
||||
"discount2": 8.0,
|
||||
"discount3": 4.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(self.po_line.discount1, 12.0)
|
||||
self.assertEqual(self.po_line.discount2, 8.0)
|
||||
self.assertEqual(self.po_line.discount3, 4.0)
|
||||
expected = 100 - (100 * 0.88 * 0.92 * 0.96)
|
||||
self.assertAlmostEqual(self.po_line.discount, expected, places=2)
|
||||
|
||||
def test_po_line_price_calculation(self):
|
||||
"""The PO subtotal applies the total discount"""
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 15.0,
|
||||
"discount2": 10.0,
|
||||
"discount3": 5.0,
|
||||
}
|
||||
)
|
||||
|
||||
# The PO line keeps the total discount with the "Discount" precision
|
||||
# (27.33 %, not 27.3325 %), so the subtotal is computed from it.
|
||||
expected_subtotal = 10 * 150 * (1 - self.po_line.discount / 100)
|
||||
self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2)
|
||||
|
||||
def test_multiple_po_lines(self):
|
||||
"""Test multiple PO lines with different discounts"""
|
||||
line2 = self.env["purchase.order.line"].create(
|
||||
{
|
||||
"order_id": self.purchase_order.id,
|
||||
"product_id": self.product.id,
|
||||
"product_qty": 5,
|
||||
"price_unit": 120.0,
|
||||
"discount1": 20.0,
|
||||
"discount2": 15.0,
|
||||
"discount3": 10.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Verify both lines have correct discounts
|
||||
self.assertEqual(self.po_line.discount1, 10.0)
|
||||
self.assertEqual(line2.discount1, 20.0)
|
||||
self.assertEqual(line2.discount2, 15.0)
|
||||
self.assertEqual(line2.discount3, 10.0)
|
||||
|
||||
def test_po_line_update_quantity(self):
|
||||
"""Test updating quantity doesn't affect discounts"""
|
||||
initial_discount1 = self.po_line.discount1
|
||||
initial_discount2 = self.po_line.discount2
|
||||
|
||||
self.po_line.write(
|
||||
{
|
||||
"product_qty": 20,
|
||||
}
|
||||
)
|
||||
|
||||
# Discounts should remain unchanged
|
||||
self.assertEqual(self.po_line.discount1, initial_discount1)
|
||||
self.assertEqual(self.po_line.discount2, initial_discount2)
|
||||
# Quantity should be updated
|
||||
self.assertEqual(self.po_line.product_qty, 20)
|
||||
|
||||
def test_po_line_update_price(self):
|
||||
"""Test updating price doesn't affect discounts"""
|
||||
initial_discount1 = self.po_line.discount1
|
||||
|
||||
self.po_line.write(
|
||||
{
|
||||
"price_unit": 200.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Discount should remain unchanged
|
||||
self.assertEqual(self.po_line.discount1, initial_discount1)
|
||||
# Price should be updated
|
||||
self.assertEqual(self.po_line.price_unit, 200.0)
|
||||
|
||||
def test_po_with_zero_discounts(self):
|
||||
"""Test PO line with all zero discounts"""
|
||||
self.po_line.write(
|
||||
{
|
||||
"discount1": 0.0,
|
||||
"discount2": 0.0,
|
||||
"discount3": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
# All discounts should be zero
|
||||
self.assertEqual(self.po_line.discount, 0.0)
|
||||
self.assertEqual(self.po_line.discount1, 0.0)
|
||||
self.assertEqual(self.po_line.discount2, 0.0)
|
||||
self.assertEqual(self.po_line.discount3, 0.0)
|
||||
|
||||
# Subtotal should be quantity * price
|
||||
expected = 10 * 150
|
||||
self.assertEqual(self.po_line.price_subtotal, expected)
|
||||
|
||||
def test_po_confirm_with_discounts(self):
|
||||
"""Test confirming PO doesn't alter discounts"""
|
||||
# Confirm the purchase order
|
||||
self.purchase_order.button_confirm()
|
||||
|
||||
# Discounts should remain unchanged after confirmation
|
||||
self.assertEqual(self.po_line.discount1, 10.0)
|
||||
self.assertEqual(self.po_line.discount2, 5.0)
|
||||
self.assertEqual(self.po_line.discount3, 2.0)
|
||||
|
||||
def test_bill_keeps_the_three_discounts(self):
|
||||
"""The bill line gets discount1..3 of the PO line, not their sum"""
|
||||
bill_line = self._create_bill_line()
|
||||
|
||||
self.assertEqual(len(bill_line), 1)
|
||||
self.assertEqual(bill_line.discount1, 10.0)
|
||||
self.assertEqual(bill_line.discount2, 5.0)
|
||||
self.assertEqual(bill_line.discount3, 2.0)
|
||||
# The bill line computes the total discount without rounding it.
|
||||
expected = 10 * 150 * 0.90 * 0.95 * 0.98
|
||||
self.assertAlmostEqual(bill_line.price_subtotal, expected, places=2)
|
||||
|
||||
def test_bill_line_edit_keeps_the_other_discounts(self):
|
||||
"""Editing one discount on the bill keeps the other two"""
|
||||
bill_line = self._create_bill_line()
|
||||
|
||||
bill_line.write({"discount": 50.0, "discount2": 12.0})
|
||||
|
||||
self.assertEqual(bill_line.discount1, 10.0)
|
||||
self.assertEqual(bill_line.discount2, 12.0)
|
||||
self.assertEqual(bill_line.discount3, 2.0)
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
# Copyright (C) 2025: Criptomart (https://criptomart.net)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestTripleDiscountMixin(TransactionCase):
|
||||
"""The patched ``triple.discount.mixin``, through an invoice line.
|
||||
|
||||
Purchase lines do not use this mixin in 18.0 (see test_purchase_order).
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create a partner
|
||||
cls.partner = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Partner",
|
||||
}
|
||||
)
|
||||
|
||||
# Create a product template first, then get the variant
|
||||
cls.product_template = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Test Product",
|
||||
"type": "consu",
|
||||
"list_price": 100.0,
|
||||
"standard_price": 50.0,
|
||||
}
|
||||
)
|
||||
# Get the auto-created product variant
|
||||
cls.product = cls.product_template.product_variant_ids[0]
|
||||
|
||||
# Create an invoice
|
||||
cls.invoice = cls.env["account.move"].create(
|
||||
{
|
||||
"move_type": "out_invoice",
|
||||
"partner_id": cls.partner.id,
|
||||
"invoice_date": "2026-01-01",
|
||||
}
|
||||
)
|
||||
|
||||
# Create an invoice line
|
||||
cls.line = cls.env["account.move.line"].create(
|
||||
{
|
||||
"move_id": cls.invoice.id,
|
||||
"product_id": cls.product.id,
|
||||
"quantity": 10,
|
||||
"price_unit": 100.0,
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
"discount3": 2.0,
|
||||
}
|
||||
)
|
||||
|
||||
def test_discount_field_is_readonly(self):
|
||||
"""Test that the discount field is readonly"""
|
||||
field = self.line._fields["discount"]
|
||||
self.assertTrue(field.readonly, "Discount field should be readonly")
|
||||
|
||||
def test_write_with_explicit_discounts(self):
|
||||
"""Test writing with explicit discount1, discount2, discount3"""
|
||||
# Write with explicit discounts
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 20.0, # This should be ignored
|
||||
"discount1": 15.0,
|
||||
"discount2": 10.0,
|
||||
"discount3": 5.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Verify explicit discounts were applied
|
||||
self.assertEqual(self.line.discount1, 15.0)
|
||||
self.assertEqual(self.line.discount2, 10.0)
|
||||
self.assertEqual(self.line.discount3, 5.0)
|
||||
|
||||
# The computed discount field should reflect the combined discounts
|
||||
# Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05))
|
||||
expected_discount = 100 - (100 * 0.85 * 0.90 * 0.95)
|
||||
self.assertAlmostEqual(self.line.discount, expected_discount, places=2)
|
||||
|
||||
def test_write_only_discount1(self):
|
||||
"""Test writing only discount1 explicitly"""
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 25.0, # This should be ignored
|
||||
"discount1": 20.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Only discount1 should change
|
||||
self.assertEqual(self.line.discount1, 20.0)
|
||||
# Others should remain unchanged
|
||||
self.assertEqual(self.line.discount2, 5.0)
|
||||
self.assertEqual(self.line.discount3, 2.0)
|
||||
|
||||
def test_write_only_discount2(self):
|
||||
"""Test writing only discount2 explicitly"""
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 30.0, # This should be ignored
|
||||
"discount2": 12.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Only discount2 should change; each test starts from setUpClass
|
||||
self.assertEqual(self.line.discount2, 12.0)
|
||||
self.assertEqual(self.line.discount1, 10.0)
|
||||
self.assertEqual(self.line.discount3, 2.0)
|
||||
|
||||
def test_write_only_discount3(self):
|
||||
"""Test writing only discount3 explicitly"""
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 35.0, # This should be ignored
|
||||
"discount3": 8.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Only discount3 should change; each test starts from setUpClass
|
||||
self.assertEqual(self.line.discount3, 8.0)
|
||||
self.assertEqual(self.line.discount1, 10.0)
|
||||
self.assertEqual(self.line.discount2, 5.0)
|
||||
|
||||
def test_write_legacy_discount_only(self):
|
||||
"""Test legacy behavior: writing only discount field"""
|
||||
# Reset to known state first
|
||||
self.line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 5.0,
|
||||
"discount3": 2.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Write only discount (legacy behavior)
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 25.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Should map to discount1 and reset others
|
||||
self.assertEqual(self.line.discount1, 25.0)
|
||||
self.assertEqual(self.line.discount2, 0.0)
|
||||
self.assertEqual(self.line.discount3, 0.0)
|
||||
|
||||
def test_write_multiple_times(self):
|
||||
"""Test writing multiple times to ensure consistency"""
|
||||
# First write
|
||||
self.line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 10.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(self.line.discount1, 10.0)
|
||||
self.assertEqual(self.line.discount2, 10.0)
|
||||
|
||||
# Second write
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 5.0,
|
||||
"discount3": 5.0,
|
||||
}
|
||||
)
|
||||
|
||||
# discount3 should change, others remain
|
||||
self.assertEqual(self.line.discount1, 10.0)
|
||||
self.assertEqual(self.line.discount2, 10.0)
|
||||
self.assertEqual(self.line.discount3, 5.0)
|
||||
|
||||
def test_write_zero_discounts(self):
|
||||
"""Test writing zero discounts"""
|
||||
self.line.write(
|
||||
{
|
||||
"discount1": 0.0,
|
||||
"discount2": 0.0,
|
||||
"discount3": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(self.line.discount1, 0.0)
|
||||
self.assertEqual(self.line.discount2, 0.0)
|
||||
self.assertEqual(self.line.discount3, 0.0)
|
||||
self.assertEqual(self.line.discount, 0.0)
|
||||
|
||||
def test_write_combined_scenario(self):
|
||||
"""Test a realistic combined scenario"""
|
||||
# Initial state
|
||||
self.line.write(
|
||||
{
|
||||
"discount1": 15.0,
|
||||
"discount2": 5.0,
|
||||
"discount3": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
# User tries to update discount field (should be ignored if explicit discounts present)
|
||||
self.line.write(
|
||||
{
|
||||
"discount": 50.0,
|
||||
"discount1": 20.0,
|
||||
}
|
||||
)
|
||||
|
||||
# discount1 should be updated, others unchanged
|
||||
self.assertEqual(self.line.discount1, 20.0)
|
||||
self.assertEqual(self.line.discount2, 5.0)
|
||||
self.assertEqual(self.line.discount3, 0.0)
|
||||
|
||||
def test_discount_calculation_accuracy(self):
|
||||
"""Test that discount calculation is accurate"""
|
||||
self.line.write(
|
||||
{
|
||||
"discount1": 10.0,
|
||||
"discount2": 10.0,
|
||||
"discount3": 10.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1
|
||||
expected = 100 - (100 * 0.9 * 0.9 * 0.9)
|
||||
self.assertAlmostEqual(self.line.discount, expected, places=2)
|
||||
|
||||
def test_write_without_discount_field(self):
|
||||
"""Test writing other fields without touching discount fields"""
|
||||
initial_discount1 = self.line.discount1
|
||||
|
||||
# Write other fields
|
||||
self.line.write(
|
||||
{
|
||||
"quantity": 20,
|
||||
"price_unit": 150.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Discounts should remain unchanged
|
||||
self.assertEqual(self.line.discount1, initial_discount1)
|
||||
# But other fields should be updated
|
||||
self.assertEqual(self.line.quantity, 20)
|
||||
self.assertEqual(self.line.price_unit, 150.0)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Make discount field readonly in invoice lines -->
|
||||
<!-- Make discount field readonly in invoice lines form view -->
|
||||
<record id="invoice_triple_discount_readonly" model="ir.ui.view">
|
||||
<field name="name">account.invoice.triple.discount.readonly</field>
|
||||
<field name="model">account.move</field>
|
||||
|
|
@ -10,12 +10,7 @@
|
|||
ref="account_invoice_triple_discount.invoice_triple_discount_form_view"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='invoice_line_ids']//tree//field[@name='discount']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<!-- Make the total discount field readonly in form view to force users to use discount1/2/3 -->
|
||||
<xpath
|
||||
expr="//field[@name='invoice_line_ids']//form//field[@name='discount']"
|
||||
position="attributes"
|
||||
|
|
|
|||
|
|
@ -1,32 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Make discount field readonly in supplier info form view -->
|
||||
<record model="ir.ui.view" id="product_supplierinfo_form_view_readonly_discount">
|
||||
<field name="model">product.supplierinfo</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="purchase_triple_discount.product_supplierinfo_form_view"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='discount']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Make discount field readonly in supplier info tree view -->
|
||||
<record model="ir.ui.view" id="product_supplierinfo_tree_view_readonly_discount">
|
||||
<field name="model">product.supplierinfo</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="purchase_triple_discount.product_supplierinfo_tree_view"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='discount']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<!-- No need to modify product.supplierinfo views as purchase_triple_discount already hides discount field -->
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Make discount field readonly in purchase order lines -->
|
||||
<record id="purchase_order_triple_discount_readonly" model="ir.ui.view">
|
||||
<field name="name">purchase.order.triple.discount.readonly</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="purchase_triple_discount.purchase_order_triple_discount_form_view"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='order_line']//tree//field[@name='discount']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//field[@name='order_line']//form//field[@name='discount']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<!-- No need to modify purchase.order views as purchase_triple_discount already hides discount field -->
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Make default_supplierinfo_discount readonly in partner form view -->
|
||||
<record model="ir.ui.view" id="res_partner_form_view_readonly_discount">
|
||||
<field name="model">res.partner</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="purchase_triple_discount.res_partner_form_view"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="default_supplierinfo_discount" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<!-- No need to modify partner view as purchase_triple_discount already uses discount1/2/3 fields -->
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
==============================
|
||||
Generate Barcodes for Partners
|
||||
==============================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:0c984bdd1b103633bb285af4d69763047cc898ed9b6e3499c164a41d8e300f99
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--barcode-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/stock-logistics-barcode/tree/12.0/barcodes_generator_partner
|
||||
:alt: OCA/stock-logistics-barcode
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/stock-logistics-barcode-12-0/stock-logistics-barcode-12-0-barcodes_generator_partner
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-barcode&target_branch=12.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module expands Odoo functionality, allowing user to generate barcode
|
||||
depending on a given barcode rule for Partners.
|
||||
|
||||
For example, a typical pattern for partners is "042........." that means
|
||||
that:
|
||||
|
||||
* the EAN13 code will begin by '042'
|
||||
* followed by 0 digits (named Barcode Base in this module)
|
||||
* a 13 digit control
|
||||
|
||||
With this module, it is possible to:
|
||||
|
||||
* Assign a pattern (barcode.rule) to a res.partner
|
||||
|
||||
* Define a Barcode base:
|
||||
* manually, if the base of the barcode must be set by a user. (typically an
|
||||
internal code defined in your company)
|
||||
* automaticaly by a sequence, if you want to let Odoo to increment a
|
||||
sequence. (typical case of a customer number incrementation)
|
||||
|
||||
* Generate a barcode, based on the defined pattern and the barcode base
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
* To configure this module, see the 'Configuration' Section of the description of the module 'barcodes_generator_abstract'
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
* Go to a Customer/Contact form, Sales & Purchases Tab:
|
||||
|
||||
1 for manual generation
|
||||
* Set a Barcode Rule
|
||||
* Set a Barcode Base
|
||||
* click on the button 'Generate Barcode (Using Barcode Rule)'
|
||||
|
||||
2 for automatic generation
|
||||
* Set a Barcode Rule
|
||||
* click on the button 'Generate Base (Using Sequence)'
|
||||
* click on the button 'Generate Barcode (Using Barcode Rule)'
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/barcodes_generator_partner/static/description/res_partner_sequence_generation.png
|
||||
:width: 1100px
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-barcode/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/stock-logistics-barcode/issues/new?body=module:%20barcodes_generator_partner%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* GRAP
|
||||
* La Louve
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
* Dave Lasley <dave@laslabs.com>
|
||||
* Druidoo (https://druidoo.io)
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/stock-logistics-barcode <https://github.com/OCA/stock-logistics-barcode/tree/12.0/barcodes_generator_partner>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import models
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Copyright (C) 2014-Today GRAP (http://www.grap.coop)
|
||||
# Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
{
|
||||
"name": "Generate Barcodes for Partners",
|
||||
"summary": "Generate Barcodes for Partners",
|
||||
"version": "16.0.1.0.1",
|
||||
"category": "Tools",
|
||||
"author": "GRAP," "La Louve," "Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/stock-logistics-barcode",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"barcodes_generator_abstract",
|
||||
"point_of_sale",
|
||||
],
|
||||
"data": [
|
||||
"views/view_res_partner.xml",
|
||||
],
|
||||
"demo": [
|
||||
"demo/ir_sequence.xml",
|
||||
"demo/barcode_rule.xml",
|
||||
"demo/res_partner.xml",
|
||||
"demo/function.xml",
|
||||
],
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (C) 2016-Today: GRAP (http://www.grap.coop)
|
||||
Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="partner_generated_barcode" model="barcode.rule">
|
||||
<field name="name">Partner Rule (Generated Barcode)</field>
|
||||
<field
|
||||
name="barcode_nomenclature_id"
|
||||
ref="barcodes.default_barcode_nomenclature"
|
||||
/>
|
||||
<field name="type">client</field>
|
||||
<field name="sequence">998</field>
|
||||
<field name="encoding">ean13</field>
|
||||
<field name="pattern">042.........</field>
|
||||
<field name="generate_type" eval="'sequence'" />
|
||||
<field name="generate_model" eval="'res.partner'" />
|
||||
<field name="sequence_id" ref="partner_ir_sequence" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (C) 2016-Today: GRAP (http://www.grap.coop)
|
||||
Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<function
|
||||
model="res.partner"
|
||||
name="generate_base"
|
||||
eval="[ref('res_partner_barcode')]"
|
||||
/>
|
||||
|
||||
<function
|
||||
model="res.partner"
|
||||
name="generate_barcode"
|
||||
eval="[ref('res_partner_barcode')]"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (C) 2016-Today: GRAP (http://www.grap.coop)
|
||||
Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="partner_ir_sequence" model="ir.sequence">
|
||||
<field name="name">Partner Sequence (Generated Barcode)</field>
|
||||
<field name="padding">10</field>
|
||||
<field name="number_next">1</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (C) 2016-Today: GRAP (http://www.grap.coop)
|
||||
Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="res_partner_barcode" model="res.partner">
|
||||
<field name="name">Partner with Generated Barcode</field>
|
||||
<field name="barcode_rule_id" ref="partner_generated_barcode" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * barcodes_generator_partner
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 12.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Barcode (Using Barcode Rule)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Base (Using Sequence)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "Generate Model"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,help:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "If 'Generate Type' is set, mention the model related to this rule."
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Lot"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Partners"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Stock Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Unit Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Weighted Product"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * barcodes_generator_partner
|
||||
#
|
||||
# Translators:
|
||||
# enjolras <yo@miguelrevilla.com>, 2018
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-02-15 02:01+0000\n"
|
||||
"PO-Revision-Date: 2018-02-15 02:01+0000\n"
|
||||
"Last-Translator: enjolras <yo@miguelrevilla.com>, 2018\n"
|
||||
"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr "Regla de código de barras"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Barcode (Using Barcode Rule)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Base (Using Sequence)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "Generate Model"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,help:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "If 'Generate Type' is set, mention the model related to this rule."
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Lot"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Partners"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Stock Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Unit Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Weighted Product"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Barcode Base"
|
||||
#~ msgstr "Base de código de barras"
|
||||
|
||||
#~ msgid "barcode.rule"
|
||||
#~ msgstr "barcode.rule"
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * barcodes_generator_partner
|
||||
#
|
||||
# Translators:
|
||||
# OCA Transbot <transbot@odoo-community.org>, 2017
|
||||
# Fernando Lara <gennesis45@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-05-02 00:27+0000\n"
|
||||
"PO-Revision-Date: 2017-05-02 00:27+0000\n"
|
||||
"Last-Translator: Fernando Lara <gennesis45@gmail.com>, 2017\n"
|
||||
"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/"
|
||||
"es_ES/)\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr "Regla del codigo de barras"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Barcode (Using Barcode Rule)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Base (Using Sequence)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
#, fuzzy
|
||||
msgid "Generate Model"
|
||||
msgstr "Generar Tipo"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,help:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "If 'Generate Type' is set, mention the model related to this rule."
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Lot"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Partners"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Stock Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Unit Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Weighted Product"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Allow to generate barcode, including a number (a base) in the final "
|
||||
#~ "barcode.\n"
|
||||
#~ " 'Base Set Manually' : User should set manually the value of the barcode "
|
||||
#~ "base\n"
|
||||
#~ " 'Base managed by Sequence': User will use a button to generate a new "
|
||||
#~ "base. This base will be generated by a sequence"
|
||||
#~ msgstr ""
|
||||
#~ "Permitir generar código de barras, incluyendo un número (una base) en el "
|
||||
#~ "código de barras final.'U+23CE'\n"
|
||||
#~ "'Base Set Manually': El usuario debe establecer manualmente el valor del "
|
||||
#~ "código de barras base'U+23CE'\n"
|
||||
#~ "'Base administrada por Secuencia': El usuario utilizará un botón para "
|
||||
#~ "generar una nueva base. Esta base será generada por una secuencia"
|
||||
|
||||
#~ msgid "Barcode Base"
|
||||
#~ msgstr "Base de código de barras"
|
||||
|
||||
#~ msgid "barcode.rule"
|
||||
#~ msgstr "regla.barra de codigos"
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * barcodes_generator_partner
|
||||
#
|
||||
# Translators:
|
||||
# OCA Transbot <transbot@odoo-community.org>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-05-02 00:27+0000\n"
|
||||
"PO-Revision-Date: 2017-05-02 00:27+0000\n"
|
||||
"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2017\n"
|
||||
"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr "Règle de code barre"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Barcode (Using Barcode Rule)"
|
||||
msgstr "Générer un code barre (via règle de codes barre)"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Base (Using Sequence)"
|
||||
msgstr "Générer une base (via une séquence)"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
#, fuzzy
|
||||
msgid "Generate Model"
|
||||
msgstr "Type de génération"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,help:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "If 'Generate Type' is set, mention the model related to this rule."
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Lot"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
#, fuzzy
|
||||
msgid "Partners"
|
||||
msgstr "Partenaire"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Stock Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Unit Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Weighted Product"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Allow to generate barcode, including a number (a base) in the final "
|
||||
#~ "barcode.\n"
|
||||
#~ " 'Base Set Manually' : User should set manually the value of the barcode "
|
||||
#~ "base\n"
|
||||
#~ " 'Base managed by Sequence': User will use a button to generate a new "
|
||||
#~ "base. This base will be generated by a sequence"
|
||||
#~ msgstr ""
|
||||
#~ "Autorise à générer des codes barre en incluant un numéro (base du code "
|
||||
#~ "barre) dans le code barre final.\n"
|
||||
#~ " 'Base indiquée manuellement' : L'utilisateur devra indiquer manuellement "
|
||||
#~ "la base du barcode\n"
|
||||
#~ " 'Base gérée via une séquence': L'utilisateur devra utiliser un boutton "
|
||||
#~ "pour générer une nouvelle base. Cette base sera générée par une séquence"
|
||||
|
||||
#~ msgid "Barcode Base"
|
||||
#~ msgstr "Base du code Barre"
|
||||
|
||||
#~ msgid "barcode.rule"
|
||||
#~ msgstr "barcode.rule"
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * barcodes_generator_partner
|
||||
#
|
||||
# Translators:
|
||||
# Bole <bole@dajmi5.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-02-15 02:01+0000\n"
|
||||
"PO-Revision-Date: 2018-02-15 02:01+0000\n"
|
||||
"Last-Translator: Bole <bole@dajmi5.com>, 2017\n"
|
||||
"Language-Team: Croatian (https://www.transifex.com/oca/teams/23907/hr/)\n"
|
||||
"Language: hr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr "Barkod pravilo"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Barcode (Using Barcode Rule)"
|
||||
msgstr "Generiraj barkod (koristeći pravilo)"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Base (Using Sequence)"
|
||||
msgstr "Generiraj osnovno ( koristi br.krug)"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
#, fuzzy
|
||||
msgid "Generate Model"
|
||||
msgstr "Tip generatora"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,help:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "If 'Generate Type' is set, mention the model related to this rule."
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Lot"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
#, fuzzy
|
||||
msgid "Partners"
|
||||
msgstr "Partner"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Stock Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Unit Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Weighted Product"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Allow to generate barcode, including a number (a base) in the final "
|
||||
#~ "barcode.\n"
|
||||
#~ " 'Base Set Manually' : User should set manually the value of the barcode "
|
||||
#~ "base\n"
|
||||
#~ " 'Base managed by Sequence': User will use a button to generate a new "
|
||||
#~ "base. This base will be generated by a sequence"
|
||||
#~ msgstr ""
|
||||
#~ "Omogućuje generiranje barkodova, uključujući broj(osnova) u finalnom "
|
||||
#~ "barkodu.\n"
|
||||
#~ "'Osnova postavljena ručno' : korisnik treba ručno postaviti vrijednost "
|
||||
#~ "osnovice barkoda.\n"
|
||||
#~ "'Osnova prema sekvenci' : korisnik će koristiti guzmb za generirnje nove "
|
||||
#~ "osnove. Ta osnova će biti generirana iz sekvence."
|
||||
|
||||
#~ msgid "Barcode Base"
|
||||
#~ msgstr "Osnova barkodova"
|
||||
|
||||
#~ msgid "barcode.rule"
|
||||
#~ msgstr "barcode.rule"
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * barcodes_generator_partner
|
||||
#
|
||||
# Translators:
|
||||
# Peter Hageman <hageman.p@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-07-07 15:18+0000\n"
|
||||
"PO-Revision-Date: 2017-07-07 15:18+0000\n"
|
||||
"Last-Translator: Peter Hageman <hageman.p@gmail.com>, 2017\n"
|
||||
"Language-Team: Dutch (Netherlands) (https://www.transifex.com/oca/"
|
||||
"teams/23907/nl_NL/)\n"
|
||||
"Language: nl_NL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_barcode_rule
|
||||
msgid "Barcode Rule"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model,name:barcodes_generator_partner.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Barcode (Using Barcode Rule)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model_terms:ir.ui.view,arch_db:barcodes_generator_partner.view_res_partner_form
|
||||
msgid "Generate Base (Using Sequence)"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "Generate Model"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,help:barcodes_generator_partner.field_barcode_rule__generate_model
|
||||
msgid "If 'Generate Type' is set, mention the model related to this rule."
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Lot"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Package"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Partners"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,generate_model:0
|
||||
msgid "Stock Location"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: model:ir.model.fields,field_description:barcodes_generator_partner.field_barcode_rule__type
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Unit Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: barcodes_generator_partner
|
||||
#: selection:barcode.rule,type:0
|
||||
msgid "Weighted Product"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "barcode.rule"
|
||||
#~ msgstr "barcode.rule"
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import barcode_rule
|
||||
from . import res_partner
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# Copyright (C) 2014-Today GRAP (http://www.grap.coop)
|
||||
# Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class BarcodeRule(models.Model):
|
||||
_inherit = "barcode.rule"
|
||||
|
||||
generate_model = fields.Selection(selection_add=[("res.partner", "Partners")])
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Copyright (C) 2014-Today GRAP (http://www.grap.coop)
|
||||
# Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_name = "res.partner"
|
||||
_inherit = ["res.partner", "barcode.generate.mixin"]
|
||||
|
|
@ -1 +0,0 @@
|
|||
* To configure this module, see the 'Configuration' Section of the description of the module 'barcodes_generator_abstract'
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
* Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
* Dave Lasley <dave@laslabs.com>
|
||||
* Druidoo (https://druidoo.io)
|
||||
* Armand POLMARD (https://github.com/ArPol-developpement)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
This module expands Odoo functionality, allowing user to generate barcode
|
||||
depending on a given barcode rule for Partners.
|
||||
|
||||
For example, a typical pattern for partners is "042........." that means
|
||||
that:
|
||||
|
||||
* the EAN13 code will begin by '042'
|
||||
* followed by 0 digits (named Barcode Base in this module)
|
||||
* a 13 digit control
|
||||
|
||||
With this module, it is possible to:
|
||||
|
||||
* Assign a pattern (barcode.rule) to a res.partner
|
||||
|
||||
* Define a Barcode base:
|
||||
* manually, if the base of the barcode must be set by a user. (typically an
|
||||
internal code defined in your company)
|
||||
* automaticaly by a sequence, if you want to let Odoo to increment a
|
||||
sequence. (typical case of a customer number incrementation)
|
||||
|
||||
* Generate a barcode, based on the defined pattern and the barcode base
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
To use this module, you need to:
|
||||
|
||||
* Go to a Customer/Contact form, Sales & Purchases Tab:
|
||||
|
||||
1 for manual generation
|
||||
* Set a Barcode Rule
|
||||
* Set a Barcode Base
|
||||
* click on the button 'Generate Barcode (Using Barcode Rule)'
|
||||
|
||||
2 for automatic generation
|
||||
* Set a Barcode Rule
|
||||
* click on the button 'Generate Base (Using Sequence)'
|
||||
* click on the button 'Generate Barcode (Using Barcode Rule)'
|
||||
|
||||
.. image:: /barcodes_generator_partner/static/description/res_partner_sequence_generation.png
|
||||
:width: 1100px
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,480 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Generate Barcodes for Partners</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="generate-barcodes-for-partners">
|
||||
<h1 class="title">Generate Barcodes for Partners</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:0c984bdd1b103633bb285af4d69763047cc898ed9b6e3499c164a41d8e300f99
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/stock-logistics-barcode/tree/12.0/barcodes_generator_partner"><img alt="OCA/stock-logistics-barcode" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--barcode-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-barcode-12-0/stock-logistics-barcode-12-0-barcodes_generator_partner"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-barcode&target_branch=12.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This module expands Odoo functionality, allowing user to generate barcode
|
||||
depending on a given barcode rule for Partners.</p>
|
||||
<p>For example, a typical pattern for partners is “042………” that means
|
||||
that:</p>
|
||||
<ul class="simple">
|
||||
<li>the EAN13 code will begin by ‘042’</li>
|
||||
<li>followed by 0 digits (named Barcode Base in this module)</li>
|
||||
<li>a 13 digit control</li>
|
||||
</ul>
|
||||
<p>With this module, it is possible to:</p>
|
||||
<ul class="simple">
|
||||
<li>Assign a pattern (barcode.rule) to a res.partner</li>
|
||||
<li><dl class="first docutils">
|
||||
<dt>Define a Barcode base:</dt>
|
||||
<dd><ul class="first last">
|
||||
<li>manually, if the base of the barcode must be set by a user. (typically an
|
||||
internal code defined in your company)</li>
|
||||
<li>automaticaly by a sequence, if you want to let Odoo to increment a
|
||||
sequence. (typical case of a customer number incrementation)</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Generate a barcode, based on the defined pattern and the barcode base</li>
|
||||
</ul>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<ul class="simple">
|
||||
<li>To configure this module, see the ‘Configuration’ Section of the description of the module ‘barcodes_generator_abstract’</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>To use this module, you need to:</p>
|
||||
<ul class="simple">
|
||||
<li>Go to a Customer/Contact form, Sales & Purchases Tab:</li>
|
||||
</ul>
|
||||
<dl class="docutils">
|
||||
<dt>1 for manual generation</dt>
|
||||
<dd><ul class="first last simple">
|
||||
<li>Set a Barcode Rule</li>
|
||||
<li>Set a Barcode Base</li>
|
||||
<li>click on the button ‘Generate Barcode (Using Barcode Rule)’</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>2 for automatic generation</dt>
|
||||
<dd><ul class="first last simple">
|
||||
<li>Set a Barcode Rule</li>
|
||||
<li>click on the button ‘Generate Base (Using Sequence)’</li>
|
||||
<li>click on the button ‘Generate Barcode (Using Barcode Rule)’</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
<img alt="https://raw.githubusercontent.com/barcodes_generator_partner/static/description/res_partner_sequence_generation.png" src="https://raw.githubusercontent.com/barcodes_generator_partner/static/description/res_partner_sequence_generation.png" style="width: 1100px;" />
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-barcode/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/stock-logistics-barcode/issues/new?body=module:%20barcodes_generator_partner%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>GRAP</li>
|
||||
<li>La Louve</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Sylvain LE GAL (<a class="reference external" href="https://twitter.com/legalsylvain">https://twitter.com/legalsylvain</a>)</li>
|
||||
<li>Dave Lasley <<a class="reference external" href="mailto:dave@laslabs.com">dave@laslabs.com</a>></li>
|
||||
<li>Druidoo (<a class="reference external" href="https://druidoo.io">https://druidoo.io</a>)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-barcode/tree/12.0/barcodes_generator_partner">OCA/stock-logistics-barcode</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import test_barcodes_generator_partner
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Copyright (C) 2016-Today GRAP (http://www.grap.coop)
|
||||
# Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class Tests(TransactionCase):
|
||||
"""Tests for 'Barcodes Generate"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner_obj = self.env["res.partner"]
|
||||
|
||||
# Test Section
|
||||
def test_01_sequence_generation_partner(self):
|
||||
self.partner = self.partner_obj.browse(
|
||||
self.ref("barcodes_generator_partner.res_partner_barcode")
|
||||
)
|
||||
self.partner.generate_barcode()
|
||||
self.assertEqual(
|
||||
self.partner.barcode_base,
|
||||
1,
|
||||
"Incorrect base Generation (by sequence) for Partner.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.partner.barcode,
|
||||
"0420000000013",
|
||||
"Barcode Generation (by sequence) for Partner."
|
||||
"Incorrect EAN13 Generated. Pattern : %s - Base : %s"
|
||||
% (self.partner.barcode_rule_id.pattern, self.partner.barcode_base),
|
||||
)
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright (C) 2016-Today: GRAP (http://www.grap.coop)
|
||||
Copyright (C) 2016-Today La Louve (http://www.lalouve.net)
|
||||
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_partner_form" model="ir.ui.view">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="barcode" position="before">
|
||||
<field
|
||||
name="barcode_rule_id"
|
||||
domain="[('generate_model', '=', 'res.partner')]"
|
||||
groups="barcodes_generator_abstract.generate_barcode"
|
||||
colspan="2"
|
||||
/>
|
||||
<field name="generate_type" invisible="1" />
|
||||
<field
|
||||
name="barcode_base"
|
||||
attrs="{
|
||||
'invisible': [('barcode_rule_id', '=', False)],
|
||||
'readonly': [('generate_type', '!=', 'manual')]}"
|
||||
groups="barcodes_generator_abstract.generate_barcode"
|
||||
colspan="2"
|
||||
/>
|
||||
<button
|
||||
name="generate_base"
|
||||
type="object"
|
||||
string="Generate Base (Using Sequence)"
|
||||
attrs="{'invisible': ['|',
|
||||
('generate_type', '!=', 'sequence'),
|
||||
('barcode_base', '!=', 0)]}"
|
||||
groups="barcodes_generator_abstract.generate_barcode"
|
||||
colspan="2"
|
||||
/>
|
||||
</field>
|
||||
|
||||
<field name="barcode" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'readonly': [('generate_type', '=', 'sequence')]}</attribute>
|
||||
</field>
|
||||
|
||||
<field name="barcode" position="after">
|
||||
<button
|
||||
name="generate_barcode"
|
||||
type="object"
|
||||
string="Generate Barcode (Using Barcode Rule)"
|
||||
attrs="{'invisible': ['|',
|
||||
('barcode_rule_id', '=', False),
|
||||
('barcode_base', '=', 0)]}"
|
||||
groups="barcodes_generator_abstract.generate_barcode"
|
||||
colspan="2"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
52
check_addon.sh
Executable file
52
check_addon.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/bin/bash
|
||||
# Script para verificar rápidamente un addon específico
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Uso: $0 <nombre_addon>"
|
||||
echo "Ejemplo: $0 account_invoice_triple_discount"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ADDON_NAME=$1
|
||||
ADDON_PATH="./$ADDON_NAME"
|
||||
|
||||
if [ ! -d "$ADDON_PATH" ]; then
|
||||
echo "Error: El addon '$ADDON_NAME' no existe en este directorio"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ADDON_PATH/__manifest__.py" ] && [ ! -f "$ADDON_PATH/__openerp__.py" ]; then
|
||||
echo "Error: '$ADDON_NAME' no parece ser un addon válido de Odoo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo "Verificando addon: $ADDON_NAME"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "1. Ejecutando black..."
|
||||
black --check "$ADDON_PATH" || (echo "❌ Black encontró problemas de formato" && black "$ADDON_PATH" && echo "✅ Formateado con black")
|
||||
|
||||
echo ""
|
||||
echo "2. Ejecutando isort..."
|
||||
isort --check-only "$ADDON_PATH" || (echo "❌ isort encontró problemas" && isort "$ADDON_PATH" && echo "✅ Imports ordenados con isort")
|
||||
|
||||
echo ""
|
||||
echo "3. Ejecutando flake8..."
|
||||
flake8 "$ADDON_PATH" && echo "✅ flake8 pasó correctamente" || echo "❌ flake8 encontró problemas"
|
||||
|
||||
echo ""
|
||||
echo "4. Ejecutando pylint (checks mandatorios)..."
|
||||
pylint --rcfile=.pylintrc-mandatory "$ADDON_PATH" && echo "✅ pylint mandatorio pasó correctamente" || echo "❌ pylint mandatorio encontró problemas"
|
||||
|
||||
echo ""
|
||||
echo "5. Ejecutando pylint (checks opcionales)..."
|
||||
pylint --rcfile=.pylintrc --exit-zero "$ADDON_PATH"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Verificación completa de $ADDON_NAME"
|
||||
echo "=========================================="
|
||||
27
check_tax_config.sh
Executable file
27
check_tax_config.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
# Verificar configuración de impuestos
|
||||
|
||||
echo "=========================================="
|
||||
echo "Verificando configuración de impuestos"
|
||||
echo "=========================================="
|
||||
|
||||
docker-compose exec -T db psql -U odoo -d odoo << 'SQL'
|
||||
-- Verificar impuestos de venta y su configuración de price_include
|
||||
SELECT
|
||||
at.id,
|
||||
at.name,
|
||||
at.amount,
|
||||
at.price_include,
|
||||
at.type_tax_use,
|
||||
rc.name as company
|
||||
FROM account_tax at
|
||||
LEFT JOIN res_company rc ON at.company_id = rc.id
|
||||
WHERE at.type_tax_use = 'sale'
|
||||
AND at.active = true
|
||||
ORDER BY at.amount DESC
|
||||
LIMIT 20;
|
||||
SQL
|
||||
|
||||
echo ""
|
||||
echo "Nota: Si price_include = false (f), entonces el precio NO incluye IVA"
|
||||
echo " Si price_include = true (t), entonces el precio SÍ incluye IVA"
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue