commit 56bfed79ac1c46e77bb7b2523b681520ed218ec2 Author: Suherdy Yacob Date: Sat Jan 3 10:15:56 2026 +0700 first commit diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..5c86589 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': 'Web Decimal Style', + 'version': '1.0', + 'category': 'Web', + 'summary': 'Style decimals differently in number fields', + 'description': """ + This module overrides the default number and monetary formatters + to wrap the decimal part in a CSS class for custom styling. + """, + 'depends': ['web', 'account'], + '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/fields/float_field.xml', + 'web_decimal_style/static/src/views/fields/monetary_field.xml', + ], + 'web.assets_frontend': [ + '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/fields/float_field.xml', + 'web_decimal_style/static/src/views/fields/monetary_field.xml', + ], + }, + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/static/src/core/utils/numbers_patch.js b/static/src/core/utils/numbers_patch.js new file mode 100644 index 0000000..af6076f --- /dev/null +++ b/static/src/core/utils/numbers_patch.js @@ -0,0 +1,37 @@ +import { localization } from "@web/core/l10n/localization"; +import { markup } from "@odoo/owl"; + +/** + * Wraps the decimal part of a formatted number string in a span for styling. + * @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"')) { + return formattedValue; + } + + const decimalPoint = localization.decimalPoint; + + if (formattedValue.includes(decimalPoint)) { + const parts = formattedValue.split(decimalPoint); + const integerPart = parts.slice(0, -1).join(decimalPoint); + const decimalPartWithSymbol = parts[parts.length - 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}${decimalPoint}${digits}${rest}`); + } else { + // Fallback: wrap the whole decimal part if it doesn't start with digits + return markup(`${integerPart}${decimalPoint}${decimalPartWithSymbol}`); + } + } + return formattedValue; +} diff --git a/static/src/css/decimal_style.css b/static/src/css/decimal_style.css new file mode 100644 index 0000000..6f39e37 --- /dev/null +++ b/static/src/css/decimal_style.css @@ -0,0 +1,5 @@ +.o_decimal { + font-size: 0.85em !important; + opacity: 0.8 !important; + display: inline-block; +} \ No newline at end of file diff --git a/static/src/views/fields/float_field.xml b/static/src/views/fields/float_field.xml new file mode 100644 index 0000000..e6e99d8 --- /dev/null +++ b/static/src/views/fields/float_field.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/src/views/fields/formatters_patch.js b/static/src/views/fields/formatters_patch.js new file mode 100644 index 0000000..7ba73ae --- /dev/null +++ b/static/src/views/fields/formatters_patch.js @@ -0,0 +1,89 @@ +import { registry } from "@web/core/registry"; +import { patch } from "@web/core/utils/patch"; +import { wrapDecimal } from "@web_decimal_style/core/utils/numbers_patch"; +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"; + +// Patch Components for Form Views +patch(MonetaryField.prototype, { + get formattedValue() { + if (this.props.inputType === "number" && !this.props.readonly && this.value) { + return this.value; + } + const res = formatMonetary(this.value, { + digits: this.currencyDigits, + currencyId: this.currencyId, + noSymbol: !this.props.readonly || this.props.hideSymbol, + }); + return this.props.readonly ? wrapDecimal(res) : res; + } +}); + +patch(FloatField.prototype, { + get formattedValue() { + if ( + !this.props.formatNumber || + (this.props.inputType === "number" && !this.props.readonly && this.value) + ) { + return this.value; + } + const options = { + digits: this.props.digits, + field: this.props.record.fields[this.props.name], + }; + let res; + if (this.props.humanReadable && !this.state.hasFocus) { + res = formatFloat(this.value, { + ...options, + humanReadable: true, + decimals: this.props.decimals, + }); + } else { + res = formatFloat(this.value, { ...options, humanReadable: false }); + } + return this.props.readonly ? wrapDecimal(res) : 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 +const formattersRegistry = registry.category("formatters"); + +function patchRegistryFormatter(name) { + if (formattersRegistry.contains(name)) { + const original = formattersRegistry.get(name); + formattersRegistry.add(name, (...args) => { + const res = original(...args); + return wrapDecimal(res); + }, { force: true }); + } +} + +patchRegistryFormatter("float"); +patchRegistryFormatter("monetary"); +patchRegistryFormatter("percentage"); +patchRegistryFormatter("float_factor"); +patchRegistryFormatter("float_time"); diff --git a/static/src/views/fields/monetary_field.xml b/static/src/views/fields/monetary_field.xml new file mode 100644 index 0000000..6416e34 --- /dev/null +++ b/static/src/views/fields/monetary_field.xml @@ -0,0 +1,8 @@ + + + + + + + +