commit 57fae57177df11988ba6e6990805f041791ca325 Author: Suherdy Yacob Date: Fri Jun 5 09:07:11 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d646835 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..53fb7b5 --- /dev/null +++ b/README.md @@ -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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..62c8060 --- /dev/null +++ b/__manifest__.py @@ -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', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..9e9e7ad --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import pos_order diff --git a/models/pos_order.py b/models/pos_order.py new file mode 100644 index 0000000..272acd1 --- /dev/null +++ b/models/pos_order.py @@ -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 diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js new file mode 100644 index 0000000..128a4c4 --- /dev/null +++ b/static/src/app/models/pos_order.js @@ -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); + } + } +}); diff --git a/static/src/app/models/pos_order_line.js b/static/src/app/models/pos_order_line.js new file mode 100644 index 0000000..2cef510 --- /dev/null +++ b/static/src/app/models/pos_order_line.js @@ -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; + } +}); diff --git a/static/src/app/services/pos_store.js b/static/src/app/services/pos_store.js new file mode 100644 index 0000000..2b993c1 --- /dev/null +++ b/static/src/app/services/pos_store.js @@ -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; + } +}); diff --git a/views/product_view.xml b/views/product_view.xml new file mode 100644 index 0000000..387894e --- /dev/null +++ b/views/product_view.xml @@ -0,0 +1,16 @@ + + + + product.template.form.inherit.pos.combo.tax + product.template + + + + False + + + False + + + +