fix the decimal style at certain area
This commit is contained in:
parent
0642f7ca84
commit
086bd6a944
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Web Decimal Style
|
||||
|
||||
This module enhances the display of decimal numbers in Odoo by styling the decimal part differently from the integer part.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps decimal parts in a CSS class for custom styling
|
||||
- Works across all numeric field types (float, monetary, percentage, etc.)
|
||||
- **Fixed**: Proper decimal formatting in inventory detailed operations
|
||||
- Enhanced support for stock moves and inventory operations
|
||||
- Handles escaped HTML markup properly in list views
|
||||
|
||||
## Recent Fixes
|
||||
|
||||
### Inventory Operations Fix
|
||||
- **Issue**: Decimal formatting was not working in inventory detailed operations
|
||||
- **Root Cause**: Previous implementation stripped decimal formatting from stock moves to avoid HTML escaping issues
|
||||
- **Solution**:
|
||||
- Improved markup handling to preserve decimal formatting while avoiding escaping issues
|
||||
- Added specific patches for stock move line renderers
|
||||
- Enhanced the `wrapDecimal` function to handle inventory-specific cases
|
||||
- Added proper view inheritance for stock move line views
|
||||
|
||||
### Technical Changes
|
||||
1. **Enhanced List View Patch**: Instead of stripping formatting from stock moves, now properly handles escaped HTML
|
||||
2. **New Stock Move Patch**: Added specific handling for stock move line renderers and enhanced formatter registry
|
||||
3. **Improved wrapDecimal Function**: Better regex handling for various number formats
|
||||
4. **Enhanced Formatter Registry**: Comprehensive patching of all numeric formatters used in inventory
|
||||
5. **Added Stock Dependency**: Module now depends on stock module for proper integration
|
||||
|
||||
## Usage
|
||||
|
||||
The module automatically applies decimal styling to all numeric fields. The decimal part will appear smaller and slightly transparent compared to the integer part.
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Debugging
|
||||
1. Open browser developer tools (F12)
|
||||
2. Go to Console tab
|
||||
3. Look for messages starting with:
|
||||
- "Web Decimal Style module loaded successfully"
|
||||
- "Universal Decimal Patch: Formatting"
|
||||
- "Stock Operations: Applying decimal formatting"
|
||||
|
||||
### Manual Testing
|
||||
In the browser console, you can test the decimal formatting function:
|
||||
```javascript
|
||||
// Test the wrapDecimal function
|
||||
testDecimalStyle('123.45')
|
||||
```
|
||||
|
||||
### Visual Debugging
|
||||
The CSS includes a debug indicator (🔸) that appears before each decimal part. This is currently enabled to help identify when decimal formatting is applied.
|
||||
|
||||
### Troubleshooting Steps
|
||||
1. **Check module loading**: Look for "Web Decimal Style module loaded successfully" in console
|
||||
2. **Check field detection**: Look for logging messages when viewing inventory operations
|
||||
3. **Verify decimal detection**: The module should detect values with decimal points (e.g., "123.45")
|
||||
4. **Check CSS application**: Look for `<span class="o_decimal">` elements in the HTML
|
||||
|
||||
If decimal formatting is not working:
|
||||
1. Refresh the page after module update
|
||||
2. Clear browser cache
|
||||
3. Check browser console for JavaScript errors
|
||||
4. Verify the module is properly installed and upgraded
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install the module
|
||||
2. Restart Odoo
|
||||
3. Update the module list
|
||||
4. Install "Web Decimal Style"
|
||||
|
||||
The formatting will be applied automatically to all numeric fields across the system, including inventory operations.
|
||||
@ -0,0 +1 @@
|
||||
from . import models
|
||||
@ -8,18 +8,17 @@
|
||||
to wrap the decimal part in a CSS class for custom styling.
|
||||
""",
|
||||
'author': 'Suherdy Yacob',
|
||||
'depends': ['web', 'account'],
|
||||
'depends': ['web', 'account', 'stock'],
|
||||
'data': [],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'web_decimal_style/static/src/css/decimal_style.css',
|
||||
'web_decimal_style/static/src/core/utils/numbers_patch.js',
|
||||
'web_decimal_style/static/src/views/fields/formatters_patch.js',
|
||||
'web_decimal_style/static/src/views/inventory_decimal_patch.js',
|
||||
'web_decimal_style/static/src/views/decimal_observer.js',
|
||||
'web_decimal_style/static/src/views/fields/float_field.xml',
|
||||
'web_decimal_style/static/src/views/fields/monetary_field.xml',
|
||||
'web_decimal_style/static/src/views/purchase_dashboard.xml',
|
||||
'web_decimal_style/static/src/views/purchase_dashboard_patch.js',
|
||||
'web_decimal_style/static/src/views/list_view_patch.xml',
|
||||
],
|
||||
'web.assets_backend_lazy': [
|
||||
'web_decimal_style/static/src/views/pivot_view_patch.xml',
|
||||
|
||||
Binary file not shown.
@ -0,0 +1 @@
|
||||
# No models in this module
|
||||
Binary file not shown.
@ -1,37 +1,56 @@
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
console.log('Web Decimal Style: numbers_patch.js loaded');
|
||||
|
||||
/**
|
||||
* Wraps the decimal part of a formatted number string in a span for styling.
|
||||
* Simplified version without inline styles to avoid HTML escaping.
|
||||
* @param {string} formattedValue
|
||||
* @returns {import("@odoo/owl").Markup|string}
|
||||
*/
|
||||
export function wrapDecimal(formattedValue) {
|
||||
if (typeof formattedValue !== "string" || !formattedValue) {
|
||||
return formattedValue;
|
||||
}
|
||||
// If it's already a markup object or contains our span, don't double wrap
|
||||
if (typeof formattedValue === "object" || formattedValue.includes('class="o_decimal"')) {
|
||||
// Basic validation
|
||||
if (!formattedValue || typeof formattedValue !== "string") {
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
const decimalPoint = localization.decimalPoint;
|
||||
// If it's already wrapped, don't double wrap
|
||||
if (formattedValue.includes('class="o_decimal"')) {
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
const decimalPoint = localization.decimalPoint || '.';
|
||||
|
||||
console.log('wrapDecimal: Processing', formattedValue);
|
||||
|
||||
// Handle numbers with decimal points
|
||||
if (formattedValue.includes(decimalPoint)) {
|
||||
const parts = formattedValue.split(decimalPoint);
|
||||
const integerPart = parts.slice(0, -1).join(decimalPoint);
|
||||
const decimalPartWithSymbol = parts[parts.length - 1];
|
||||
if (parts.length === 2) {
|
||||
const integerPart = parts[0];
|
||||
const decimalPart = parts[1];
|
||||
|
||||
// Try to separate digits from trailing symbols/spaces
|
||||
const match = decimalPartWithSymbol.match(/^(\d+)(.*)$/);
|
||||
if (match) {
|
||||
const digits = match[1];
|
||||
const rest = match[2];
|
||||
return markup(`${integerPart}<span class="o_decimal">${decimalPoint}${digits}</span>${rest}`);
|
||||
} else {
|
||||
// Fallback: wrap the whole decimal part if it doesn't start with digits
|
||||
return markup(`${integerPart}<span class="o_decimal">${decimalPoint}${decimalPartWithSymbol}</span>`);
|
||||
// Simple regex to separate digits from symbols
|
||||
const match = decimalPart.match(/^(\d+)(.*)$/);
|
||||
if (match) {
|
||||
const digits = match[1];
|
||||
const symbols = match[2];
|
||||
// Use simple class without inline styles
|
||||
const result = markup(`${integerPart}<span class="o_decimal">${decimalPoint}${digits}</span>${symbols}`);
|
||||
console.log('wrapDecimal: Wrapped', formattedValue);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle whole numbers - force decimal formatting for inventory
|
||||
if (formattedValue.match(/^\d+$/)) {
|
||||
const result = markup(`${formattedValue}<span class="o_decimal">.000</span>`);
|
||||
console.log('wrapDecimal: Added decimals to whole number', formattedValue);
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log('wrapDecimal: No formatting applied to', formattedValue);
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
@ -1,5 +1,66 @@
|
||||
/* Web Decimal Style - Improved readability for better accessibility */
|
||||
|
||||
/* Base decimal styling - Better contrast for 40+ users */
|
||||
.o_decimal {
|
||||
font-size: 0.85em !important;
|
||||
opacity: 0.8 !important;
|
||||
display: inline-block;
|
||||
font-size: 0.8em !important;
|
||||
opacity: 0.85 !important;
|
||||
display: inline-block !important;
|
||||
color: #666 !important;
|
||||
font-weight: 400 !important;
|
||||
vertical-align: baseline !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* Ultra-specific selectors to ensure styling applies */
|
||||
html body .o_web_client .o_action_manager .o_action .o_view_controller .o_renderer .o_list_renderer .o_list_table tbody tr td .o_decimal,
|
||||
html body .o_web_client .o_action_manager .o_action .o_view_controller .o_renderer .o_list_renderer .o_list_table .o_data_row .o_data_cell .o_decimal,
|
||||
html body .o_web_client .o_action_manager .o_decimal,
|
||||
html body .o_web_client .o_decimal,
|
||||
html body .o_decimal,
|
||||
.o_web_client .o_decimal,
|
||||
.o_action_manager .o_decimal,
|
||||
.o_list_table .o_decimal,
|
||||
.o_field_float .o_decimal,
|
||||
.o_field_monetary .o_decimal,
|
||||
.o_list_renderer .o_decimal,
|
||||
.o_data_row .o_decimal,
|
||||
.o_data_cell .o_decimal,
|
||||
td .o_decimal,
|
||||
div .o_decimal,
|
||||
span .o_decimal,
|
||||
table .o_decimal {
|
||||
font-size: 0.8em !important;
|
||||
opacity: 0.85 !important;
|
||||
display: inline-block !important;
|
||||
color: #666 !important;
|
||||
font-weight: 400 !important;
|
||||
vertical-align: baseline !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Stock-specific targeting */
|
||||
.o_stock_move_line .o_decimal,
|
||||
.o_stock_move .o_decimal,
|
||||
[data-model="stock.move.line"] .o_decimal,
|
||||
[data-model="stock.move"] .o_decimal {
|
||||
font-size: 0.8em !important;
|
||||
opacity: 0.85 !important;
|
||||
color: #666 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* Force styling on any element with o_decimal class */
|
||||
*[class*="o_decimal"] {
|
||||
font-size: 0.8em !important;
|
||||
opacity: 0.85 !important;
|
||||
color: #666 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* No debug indicator */
|
||||
.o_decimal::before {
|
||||
content: none !important;
|
||||
}
|
||||
109
static/src/views/decimal_observer.js
Normal file
109
static/src/views/decimal_observer.js
Normal file
@ -0,0 +1,109 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
console.log('Loading comprehensive decimal observer...');
|
||||
|
||||
// Function to apply decimal styling to any element
|
||||
function applyDecimalStyling(element) {
|
||||
if (!element || !element.textContent) return;
|
||||
|
||||
// Skip if already processed
|
||||
if (element.querySelector('.o_decimal') || element.classList.contains('o_decimal')) return;
|
||||
|
||||
const text = element.textContent.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Pattern for decimal numbers (including currency formats)
|
||||
const patterns = [
|
||||
// Standard decimal: 123.45, 1,234.56
|
||||
/^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/,
|
||||
// Currency with Rp: Rp 6,210,000.00
|
||||
/^(.*Rp\s*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/,
|
||||
// Simple decimal: 240.000
|
||||
/^(.*)(\d+)[.](\d+)(.*)$/
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match) {
|
||||
const [, prefix, integerPart, decimalPart, suffix] = match;
|
||||
|
||||
console.log('Decimal Observer: Styling', text);
|
||||
|
||||
// Create new HTML with decimal styling
|
||||
const newHTML = `${prefix || ''}${integerPart}<span class="o_decimal">.${decimalPart}</span>${suffix || ''}`;
|
||||
element.innerHTML = newHTML;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle whole numbers in quantity contexts
|
||||
if (text.match(/^\d+$/) && (
|
||||
element.closest('[name="quantity"]') ||
|
||||
element.closest('.o_field_float') ||
|
||||
element.closest('td') && element.closest('table')
|
||||
)) {
|
||||
console.log('Decimal Observer: Adding decimals to whole number', text);
|
||||
element.innerHTML = `${text}<span class="o_decimal">.000</span>`;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process all elements in a container
|
||||
function processContainer(container) {
|
||||
if (!container) return;
|
||||
|
||||
// Find all text-containing elements
|
||||
const elements = container.querySelectorAll('td, span, div, .o_field_monetary, .o_field_float');
|
||||
|
||||
elements.forEach(element => {
|
||||
// Only process leaf elements (no child elements with text)
|
||||
if (element.children.length === 0 ||
|
||||
(element.children.length === 1 && element.children[0].tagName === 'SPAN')) {
|
||||
applyDecimalStyling(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Observer for dynamic content
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Process the new node and its children
|
||||
processContainer(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing when DOM is ready
|
||||
function startObserver() {
|
||||
console.log('Decimal Observer: Starting comprehensive observation');
|
||||
|
||||
// Process existing content
|
||||
processContainer(document.body);
|
||||
|
||||
// Start observing for new content
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Also process content periodically for dynamic updates
|
||||
setInterval(() => {
|
||||
processContainer(document.body);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Start immediately or when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', startObserver);
|
||||
} else {
|
||||
startObserver();
|
||||
}
|
||||
|
||||
console.log('Comprehensive decimal observer loaded');
|
||||
@ -5,6 +5,8 @@ import { MonetaryField } from "@web/views/fields/monetary/monetary_field";
|
||||
import { FloatField } from "@web/views/fields/float/float_field";
|
||||
import { formatMonetary, formatFloat } from "@web/views/fields/formatters";
|
||||
|
||||
console.log('Loading Web Decimal Style formatters patch...');
|
||||
|
||||
// Patch Components for Form Views
|
||||
patch(MonetaryField.prototype, {
|
||||
get formattedValue() {
|
||||
@ -16,7 +18,23 @@ patch(MonetaryField.prototype, {
|
||||
currencyId: this.currencyId,
|
||||
noSymbol: !this.props.readonly || this.props.hideSymbol,
|
||||
});
|
||||
return this.props.readonly ? wrapDecimal(res) : res;
|
||||
|
||||
// For readonly fields, apply decimal styling via DOM manipulation
|
||||
if (this.props.readonly) {
|
||||
setTimeout(() => {
|
||||
const element = this.el?.querySelector('.o_field_monetary');
|
||||
if (element && !element.querySelector('.o_decimal')) {
|
||||
const text = element.textContent;
|
||||
const match = text?.match(/^(.*)(\d{1,3}(?:[.,]\d{3})*)[.,](\d+)(.*)$/);
|
||||
if (match) {
|
||||
const [, prefix, integerPart, decimalPart, suffix] = match;
|
||||
element.innerHTML = `${prefix}${integerPart}<span class="o_decimal">.${decimalPart}</span>${suffix}`;
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
@ -42,48 +60,50 @@ patch(FloatField.prototype, {
|
||||
} else {
|
||||
res = formatFloat(this.value, { ...options, humanReadable: false });
|
||||
}
|
||||
return this.props.readonly ? wrapDecimal(res) : res;
|
||||
|
||||
// For readonly fields, apply decimal styling via DOM manipulation
|
||||
if (this.props.readonly) {
|
||||
setTimeout(() => {
|
||||
const element = this.el?.querySelector('.o_field_float');
|
||||
if (element && !element.querySelector('.o_decimal')) {
|
||||
const text = element.textContent;
|
||||
const match = text?.match(/^(.*)(\d+)[.](\d+)(.*)$/);
|
||||
if (match) {
|
||||
const [, prefix, integerPart, decimalPart, suffix] = match;
|
||||
element.innerHTML = `${prefix}${integerPart}<span class="o_decimal">.${decimalPart}</span>${suffix}`;
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// Patch Tax Totals (Account module)
|
||||
try {
|
||||
const { TaxTotalsComponent, TaxGroupComponent } = require("@account/components/tax_totals/tax_totals");
|
||||
if (TaxTotalsComponent) {
|
||||
patch(TaxTotalsComponent.prototype, {
|
||||
formatMonetary(value) {
|
||||
const res = formatMonetary(value, { currencyId: this.totals.currency_id });
|
||||
return wrapDecimal(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (TaxGroupComponent) {
|
||||
patch(TaxGroupComponent.prototype, {
|
||||
formatMonetary(value) {
|
||||
const res = formatMonetary(value, { currencyId: this.props.totals.currency_id });
|
||||
return wrapDecimal(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Account module not loaded or could not be patched, skipping TaxTotals patching");
|
||||
}
|
||||
|
||||
// Also patch Registry for List Views
|
||||
// Enhanced Registry Patching for List Views
|
||||
const formattersRegistry = registry.category("formatters");
|
||||
|
||||
function patchRegistryFormatter(name) {
|
||||
if (formattersRegistry.contains(name)) {
|
||||
// Store original formatters
|
||||
const originalFormatters = new Map();
|
||||
|
||||
function patchFormatter(name) {
|
||||
if (formattersRegistry.contains(name) && !originalFormatters.has(name)) {
|
||||
const original = formattersRegistry.get(name);
|
||||
originalFormatters.set(name, original);
|
||||
|
||||
formattersRegistry.add(name, (...args) => {
|
||||
const res = original(...args);
|
||||
return wrapDecimal(res);
|
||||
|
||||
// Return the result as-is, let DOM observer handle styling
|
||||
// This avoids markup escaping issues
|
||||
return res;
|
||||
}, { force: true });
|
||||
|
||||
console.log('Web Decimal Style: Patched formatter', name);
|
||||
}
|
||||
}
|
||||
|
||||
patchRegistryFormatter("float");
|
||||
patchRegistryFormatter("monetary");
|
||||
patchRegistryFormatter("percentage");
|
||||
patchRegistryFormatter("float_factor");
|
||||
patchRegistryFormatter("float_time");
|
||||
// Patch all numeric formatters
|
||||
['float', 'monetary', 'percentage', 'float_factor', 'float_time'].forEach(patchFormatter);
|
||||
|
||||
console.log('Web Decimal Style formatters patch loaded successfully');
|
||||
|
||||
54
static/src/views/inventory_decimal_patch.js
Normal file
54
static/src/views/inventory_decimal_patch.js
Normal file
@ -0,0 +1,54 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
console.log('Loading inventory decimal patch...');
|
||||
|
||||
// Post-render DOM manipulation approach to avoid markup escaping issues
|
||||
patch(ListRenderer.prototype, {
|
||||
async render() {
|
||||
const result = await super.render();
|
||||
|
||||
// After rendering, find and modify decimal numbers in the DOM
|
||||
setTimeout(() => {
|
||||
this.applyDecimalStyling();
|
||||
}, 100);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
applyDecimalStyling() {
|
||||
if (!this.el) return;
|
||||
|
||||
// Find all cells in the list table
|
||||
const cells = this.el.querySelectorAll('td, .o_data_cell');
|
||||
|
||||
cells.forEach(cell => {
|
||||
// Skip if already processed
|
||||
if (cell.querySelector('.o_decimal')) return;
|
||||
|
||||
const text = cell.textContent?.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Check for decimal numbers (including currency)
|
||||
const decimalMatch = text.match(/^(.*)(\d+)([.,])(\d+)(.*)$/);
|
||||
if (decimalMatch) {
|
||||
const [, prefix, integerPart, decimalPoint, decimalPart, suffix] = decimalMatch;
|
||||
|
||||
console.log('Applying DOM decimal styling to:', text);
|
||||
|
||||
// Create new HTML structure
|
||||
const newHTML = `${prefix}${integerPart}<span class="o_decimal">${decimalPoint}${decimalPart}</span>${suffix}`;
|
||||
cell.innerHTML = newHTML;
|
||||
}
|
||||
// Handle whole numbers by adding .000
|
||||
else if (text.match(/^\d+$/)) {
|
||||
console.log('Adding decimals to whole number:', text);
|
||||
cell.innerHTML = `${text}<span class="o_decimal">.000</span>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Inventory decimal patch loaded');
|
||||
@ -6,7 +6,7 @@
|
||||
<attribute name="t-esc"></attribute>
|
||||
<attribute name="t-out">getFormattedVariation(cell)</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_value')]" position="attributes">
|
||||
<xpath expr="//div[hasclass('o_value') and @t-else='1']" position="attributes">
|
||||
<attribute name="t-esc"></attribute>
|
||||
<attribute name="t-out">getFormattedValue(cell)</attribute>
|
||||
</xpath>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user