fix the decimal style at certain area

This commit is contained in:
Suherdy Yacob 2026-01-09 10:11:56 +07:00
parent 0642f7ca84
commit 086bd6a944
12 changed files with 396 additions and 58 deletions

74
README.md Normal file
View 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.

View File

@ -0,0 +1 @@
from . import models

View File

@ -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.

View File

@ -0,0 +1 @@
# No models in this module

View File

@ -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;
}

View File

@ -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;
}

View 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');

View File

@ -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');

View 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');

View File

@ -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>