first commit

This commit is contained in:
Suherdy Yacob 2026-01-03 10:15:56 +07:00
commit 56bfed79ac
6 changed files with 178 additions and 0 deletions

31
__manifest__.py Normal file
View File

@ -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',
}

View File

@ -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}<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>`);
}
}
return formattedValue;
}

View File

@ -0,0 +1,5 @@
.o_decimal {
font-size: 0.85em !important;
opacity: 0.8 !important;
display: inline-block;
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="web_decimal_style.FloatField" t-inherit="web.FloatField" t-inherit-mode="extension" owl="1">
<xpath expr="//span[@t-if='props.readonly']" position="replace">
<span t-if="props.readonly" t-out="formattedValue" />
</xpath>
</t>
</templates>

View File

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

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="web_decimal_style.MonetaryField" t-inherit="web.MonetaryField" t-inherit-mode="extension" owl="1">
<xpath expr="//span[@t-if='props.readonly']" position="replace">
<span t-if="props.readonly" t-out="formattedValue" />
</xpath>
</t>
</templates>