first commit

This commit is contained in:
Suherdy Yacob 2026-06-05 09:07:11 +07:00
commit 57fae57177
10 changed files with 187 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
__pycache__/

17
README.md Normal file
View File

@ -0,0 +1,17 @@
POS Combo Tax
=============
Odoo 19 | Custom Addon
Author: Suherdy Yacob
This module applies sales tax on the combo product itself in POS instead of child lines.
Features
--------
- Shifts pricing and tax computations to the parent combo product order line.
- Sets child combo line unit prices and taxes to zero.
- Modifies backend invoicing logic to prevent invoicing parent combo line as a section line if it has a price.
- Correctly computes margin for the combo product parent line.
- Unhides the Customer Taxes field on the combo product form so taxes can be configured directly on the combo menu.

2
__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

19
__manifest__.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name': 'POS Combo Tax',
'version': '1.0',
'category': 'Sales/Point of Sale',
'summary': 'Apply sales tax on combo products in POS',
'author': 'Suherdy Yacob',
'depends': ['point_of_sale', 'account'],
'data': [
'views/product_view.xml',
],
'assets': {
'point_of_sale._assets_pos': [
'pos_combo_tax/static/src/app/**/*',
],
},
'installable': True,
'license': 'LGPL-3',
}

2
models/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import pos_order

45
models/pos_order.py Normal file
View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.tools import float_is_zero
class PosOrder(models.Model):
_inherit = 'pos.order'
@api.model
def _get_invoice_lines_values(self, line_values, pos_line, move_type):
res = super()._get_invoice_lines_values(line_values, pos_line, move_type)
if pos_line.product_id.type == 'combo':
is_refund_order = bool(
pos_line.order_id.is_refund
or pos_line.order_id.amount_total < 0.0
)
qty_sign = -1 if (
(move_type == 'out_invoice' and is_refund_order)
or (move_type == 'out_refund' and not is_refund_order)
) else 1
res.update({
'display_type': False,
'product_id': line_values['product_id'].id,
'quantity': qty_sign * line_values['quantity'],
'discount': line_values['discount'],
'price_unit': line_values['price_unit'],
'name': line_values['name'],
'tax_ids': [(6, 0, line_values['tax_ids'].ids)],
'product_uom_id': line_values['uom_id'].id,
'extra_tax_data': self.env['account.tax']._export_base_line_extra_tax_data(line_values),
})
return res
class PosOrderLine(models.Model):
_inherit = 'pos.order.line'
@api.depends('price_subtotal', 'total_cost')
def _compute_margin(self):
super()._compute_margin()
for line in self:
if line.product_id.type == 'combo':
sign = -1 if line.order_id.is_refund else 1
line.margin = (line.price_subtotal * sign) - line.total_cost
line.margin_percent = not float_is_zero(line.price_subtotal, precision_rounding=line.currency_id.rounding) \
and line.margin / (line.price_subtotal * sign) \
or 0

View File

@ -0,0 +1,29 @@
/** @odoo-module */
import { PosOrder } from "@point_of_sale/app/models/pos_order";
import { patch } from "@web/core/utils/patch";
patch(PosOrder.prototype, {
setPricelist(pricelist) {
super.setPricelist(...arguments);
const combo_parent_lines = this.lines.filter(
(line) => line.price_type === "original" && line.combo_line_ids?.length
);
for (const line of combo_parent_lines) {
const newPrice = line.product_id.product_tmpl_id.getPrice(
pricelist,
line.getQuantity(),
line.getPriceExtra(),
false,
line.product_id
);
line.setUnitPrice(newPrice);
}
const combo_children_lines = this.lines.filter(
(line) => line.price_type === "original" && line.combo_parent_id
);
for (const line of combo_children_lines) {
line.setUnitPrice(0);
}
}
});

View File

@ -0,0 +1,22 @@
/** @odoo-module */
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
import { patch } from "@web/core/utils/patch";
patch(PosOrderline.prototype, {
get displayPrice() {
if (this.combo_line_ids && this.combo_line_ids.length > 0) {
return this.config.iface_tax_included === "total" ? this.priceIncl : this.priceExcl;
}
return super.displayPrice;
},
get displayPriceNoDiscount() {
if (this.combo_line_ids && this.combo_line_ids.length > 0) {
return this.config.iface_tax_included === "total"
? this.priceInclNoDiscount
: this.priceExclNoDiscount;
}
return super.displayPriceNoDiscount;
}
});

View File

@ -0,0 +1,33 @@
/** @odoo-module */
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { patch } from "@web/core/utils/patch";
patch(PosStore.prototype, {
handlePriceUnit(values, order, price_unit) {
if (values.product_tmpl_id.isCombo() && price_unit === undefined) {
values.price_unit = values.product_id.getPrice(
order.pricelist_id,
values.qty,
values.price_extra,
false,
values.product_id
);
} else {
super.handlePriceUnit(...arguments);
}
},
async handleComboProduct(values, order, configure = true, { line } = {}) {
const result = await super.handleComboProduct(...arguments);
if (result && values.combo_line_ids) {
for (const cmd of values.combo_line_ids) {
if (cmd[0] === "create") {
cmd[1].price_unit = 0;
cmd[1].tax_ids = [];
}
}
}
return result;
}
});

16
views/product_view.xml Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_view_inherit" model="ir.ui.view">
<field name="name">product.template.form.inherit.pos.combo.tax</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="account.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//label[@for='taxes_id']" position="attributes">
<attribute name="invisible">False</attribute>
</xpath>
<xpath expr="//div[@name='taxes_div']" position="attributes">
<attribute name="invisible">False</attribute>
</xpath>
</field>
</record>
</odoo>