9.2 KiB
9.2 KiB
Excel Image Paste Feature for Master Order Lines
Overview
This feature enables users to copy images from Excel and paste them directly into Master Order line image cells using Ctrl+V, providing an Excel-like workflow for product image management.
Feature Summary
| Aspect | Details |
|---|---|
| Target Users | Purchasing and sourcing teams |
| Use Case | Paste product images from Excel directly into order lines |
| Supported Formats | PNG, JPEG, BMP, GIF, WEBP |
| Max Image Size | Auto-resizes if > 1024x1024 pixels |
| Storage | Odoo filestore via ir.attachment |
| Browser Support | Chrome, Edge, Firefox (desktop) |
User Flow
1. Open Master Order form
2. Click on image cell in Order Lines (cell gets focus ring)
3. Copy image from Excel (Ctrl+C on image)
4. Click target image cell
5. Press Ctrl+V
6. Image appears instantly with success notification
7. Save the form to persist
Technical Architecture
Files Structure
at_master_order/
├── static/src/
│ ├── js/
│ │ ├── image_paste_widget.js # Main OWL widget
│ │ └── master_order_form.js # Form controller enhancements
│ ├── xml/
│ │ └── image_paste_templates.xml # OWL templates
│ └── scss/
│ └── image_paste.scss # Styling for paste interaction
├── models/
│ └── master_order_line.py # Backend image processing
└── views/
└── master_order_views.xml # View with widget="image_paste"
Component Diagram
┌─────────────────────────────────────────────────────────────┐
│ Browser (Frontend) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ ImagePasteField │ │ Clipboard API │ │
│ │ (OWL Widget) │◄───│ (DataTransfer/Clipboard) │ │
│ │ │ └─────────────────────────────┘ │
│ │ - onPaste() │ │
│ │ - _processImage() │ ┌─────────────────────────────┐ │
│ │ - _saveToRecord() │───►│ Canvas API │ │
│ └─────────────────────┘ │ (Image resize) │ │
│ │ └─────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ record.update() │ │
│ │ (OWL Record) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Server (Backend) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ master_order_line │───►│ ir.attachment │ │
│ │ - image_1920 field │ │ (Image storage) │ │
│ │ - write() override │ └─────────────────────────────┘ │
│ │ - _process_image() │ │
│ └─────────────────────┘ ┌─────────────────────────────┐ │
│ │ Filestore │ │
│ │ (Binary data) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Key Implementation Details
1. OWL Widget (image_paste_widget.js)
// Extends Odoo's ImageField
export class ImagePasteField extends ImageField {
static template = "at_master_order.ImagePasteField";
setup() {
super.setup();
// CRITICAL: Extend state, don't replace it!
this.state.isFocused = false;
this.state.isProcessing = false;
}
async onPaste(ev) {
// 1. Get image from clipboard
// 2. Process/resize if needed
// 3. Save via record.update()
}
}
2. Template (image_paste_templates.xml)
<t t-name="at_master_order.ImagePasteField">
<div t-ref="imagePasteContainer" tabindex="0">
<!-- Uses parent's getUrl() method for image display -->
<img t-att-src="this.getUrl(props.previewImage || props.name)"/>
</div>
</t>
3. View Configuration (master_order_views.xml)
<field name="image_1920" widget="image_paste"
options="{'size': [50, 50]}"
optional="show"
help="Click to focus, then Ctrl+V to paste image from Excel"/>
4. Model Field (master_order_line.py)
image_1920 = fields.Image('Product Image', attachment=True,
help='Upload product image for this line. Supports paste from Excel (Ctrl+V).')
Critical Lessons Learned
1. OWL State Inheritance
Problem: Overwriting this.state in child widget destroyed parent's isValid property.
// WRONG - destroys parent state
this.state = useState({ isFocused: false });
// CORRECT - extends parent state
this.state.isFocused = false;
2. ImageField URL Generation
The getUrl() method checks !this.state.isValid and returns placeholder if falsy. Parent's state must be preserved.
3. Attachment Storage
Image fields with attachment=True store data in ir_attachment table, not as a database column. Use ORM methods to access, not direct SQL.
4. Cache Invalidation
ImageField uses write_date for cache busting. Ensure fieldDependencies includes write_date.
Database Schema
ir_attachment (Image Storage)
| Column | Value |
|---|---|
| res_model | at.master.order.line |
| res_field | image_1920 |
| res_id | Line ID |
| type | binary |
| store_fname | Filestore path |
| mimetype | image/jpeg, image/png, etc. |
Query to Check Images
SELECT l.id, l.name, a.id as attachment_id, a.file_size
FROM at_master_order_line l
LEFT JOIN ir_attachment a
ON a.res_model = 'at.master.order.line'
AND a.res_field = 'image_1920'
AND a.res_id = l.id;
Image URL Format
/web/image/at.master.order.line/{line_id}/image_1920?unique={write_date}
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Placeholder shown instead of image | state.isValid is undefined |
Don't overwrite parent's this.state |
| Image not saving | Clipboard API not returning data | Check browser console, ensure HTTPS |
| 404 on image URL | Attachment not linked correctly | Check ir_attachment table |
| No paste event firing | Cell not focused | Click cell first to focus |
Testing Checklist
- Paste PNG image from Excel
- Paste JPEG image from Excel
- Paste large image (> 1024px) - should auto-resize
- Save form after paste
- Reload page - image should persist
- Delete image (Delete key when focused)
- Multiple pastes on different rows
- Paste on new unsaved line
Dependencies
Frontend
@web/views/fields/image/image_field(ImageField)@web/core/utils/hooks(useService)@odoo/owl(Component, useState, onMounted, useRef)
Backend
PIL/Pillow(Image processing)base64(Encoding)
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-01-12 | Initial implementation |
| 1.1 | 2026-01-12 | Fixed state inheritance bug |
| 1.2 | 2026-01-12 | Fixed template to use getUrl() |