diff --git a/account_portal_commission/README.rst b/account_portal_commission/README.rst new file mode 100644 index 000000000..88ff1d667 --- /dev/null +++ b/account_portal_commission/README.rst @@ -0,0 +1,9 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +========================= +Account Portal Commission +========================= + + diff --git a/account_portal_commission/__init__.py b/account_portal_commission/__init__.py new file mode 100644 index 000000000..ada0b87be --- /dev/null +++ b/account_portal_commission/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import wizards diff --git a/account_portal_commission/__manifest__.py b/account_portal_commission/__manifest__.py new file mode 100644 index 000000000..73744d924 --- /dev/null +++ b/account_portal_commission/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2025 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Account Portal Commission", + "summary": """Add account restriction to portal users to see only their invoices + and add route to show user commissions""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit, CreuBlanca", + "website": "https://github.com/tegin/cb-addons", + "depends": ["account", "account_commission", "portal", "cb_medical_commission"], + "data": [ + "views/account_portal_templates.xml", + "security/account_portal_security.xml", + "security/ir.model.access.csv", + "wizards/search_encounters_wizard_view.xml", + ], + "demo": [], + "assets": { + "web.assets_frontend": [ + "account_portal_commission/static/src/js/*.esm.js", + ], + }, +} diff --git a/account_portal_commission/controllers/__init__.py b/account_portal_commission/controllers/__init__.py new file mode 100644 index 000000000..8c3feb6f5 --- /dev/null +++ b/account_portal_commission/controllers/__init__.py @@ -0,0 +1 @@ +from . import portal diff --git a/account_portal_commission/controllers/portal.py b/account_portal_commission/controllers/portal.py new file mode 100644 index 000000000..13a0251e8 --- /dev/null +++ b/account_portal_commission/controllers/portal.py @@ -0,0 +1,87 @@ +from odoo import http +from odoo.http import request + +from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager + + +class RestrictedPortalAccount(CustomerPortal): + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if "commission_count" in counters: + commission_count = ( + request.env["account.invoice.line.agent"].search_count([]) + if request.env["account.invoice.line.agent"].check_access_rights( + "read", raise_exception=False + ) + else 0 + ) + values["commission_count"] = commission_count + return values + + @http.route( + ["/my/invoices", "/my/invoices/page/"], + type="http", + auth="user", + website=True, + ) + def portal_my_invoices( + self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw + ): + values = self._prepare_my_invoices_values( + page, date_begin, date_end, sortby, filterby + ) + + # pager + pager = portal_pager(**values["pager"]) + + # content according to pager and archive selected + invoices = values["invoices"](pager["offset"]) + request.session["my_invoices_history"] = invoices.ids[:100] + + values.update( + { + "invoices": invoices, + "pager": pager, + } + ) + + return request.render( + "account_portal_commission.portal_my_invoices_restricted", values + ) + + @http.route(["/my/commissions"], type="http", auth="user", website=True) + def portal_my_commissions(self): + settled_commission = request.env["account.invoice.line.agent"].search_read( + [("settled", "=", True)], ["amount"] + ) + unsettled_commission = request.env["account.invoice.line.agent"].search_read( + [("settled", "=", False)], ["amount"] + ) + settled_commission_ids = [x.get("id") for x in settled_commission] + invoiced_settlement = request.env["commission.settlement.line"].search_read( + [ + ("settlement_id.state", "=", "invoiced"), + ("invoice_agent_line_id", "in", settled_commission_ids), + ], + ["settled_amount"], + ) + paid_settlement = request.env["commission.settlement.line"].search_read( + [ + ("settlement_id.invoice_id.payment_state", "=", "paid"), + ("invoice_agent_line_id", "in", settled_commission_ids), + ], + ["settled_amount"], + ) + + values = { + "settled_commissions": sum([x.get("amount") for x in settled_commission]), + "unsettled_commissions": sum( + [x.get("amount") for x in unsettled_commission] + ), + "invoiced_settlement": sum( + [x.get("settled_amount") for x in invoiced_settlement] + ), + "paid_settlement": sum([x.get("settled_amount") for x in paid_settlement]), + "currency_id": request.env.company.currency_id, + } + return request.render("account_portal_commission.portal_my_commissions", values) diff --git a/account_portal_commission/models/__init__.py b/account_portal_commission/models/__init__.py new file mode 100644 index 000000000..883516533 --- /dev/null +++ b/account_portal_commission/models/__init__.py @@ -0,0 +1 @@ +from . import res_users diff --git a/account_portal_commission/models/res_users.py b/account_portal_commission/models/res_users.py new file mode 100644 index 000000000..d0a09ef28 --- /dev/null +++ b/account_portal_commission/models/res_users.py @@ -0,0 +1,17 @@ +from odoo import _, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + def action_open_wizard(self): + w = self.env["search_encounters.wizard"].create({}) + return { + "type": "ir.actions.act_window", + "target": "new", + "res_model": "search_encounters.wizard", + "name": _("Search Encounters"), + "res_id": w.id, + "views": [(False, "form")], + "context": self.env.context, + } diff --git a/account_portal_commission/pyproject.toml b/account_portal_commission/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/account_portal_commission/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_portal_commission/security/account_portal_security.xml b/account_portal_commission/security/account_portal_security.xml new file mode 100644 index 000000000..59ff1e465 --- /dev/null +++ b/account_portal_commission/security/account_portal_security.xml @@ -0,0 +1,73 @@ + + + + Account Portal Restricted Group + + + + + Account Portal Restricted Rule + + + [('partner_id', '=', user.partner_id.id)] + + + + Commission Portal Restricted Rule + + + [('agent_id', '=', user.partner_id.id)] + + + + account.portal.restricted + + + + + + + + + + commission.portal.restricted + + + + + + + + + + agent.portal.restricted + + + + + + + + + + commission.line.portal.restricted + + + + + + + + diff --git a/account_portal_commission/security/ir.model.access.csv b/account_portal_commission/security/ir.model.access.csv new file mode 100644 index 000000000..3c925c4e8 --- /dev/null +++ b/account_portal_commission/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +account_portal_commission.access_search_encounters_wizard,access_search_encounters_wizard,account_portal_commission.model_search_encounters_wizard,base.group_user,1,1,1,1 diff --git a/account_portal_commission/static/src/js/account_portal.esm.js b/account_portal_commission/static/src/js/account_portal.esm.js new file mode 100644 index 000000000..dd4241709 --- /dev/null +++ b/account_portal_commission/static/src/js/account_portal.esm.js @@ -0,0 +1,14 @@ +/** @odoo-module */ + +// force dependencies +import "portal.portal"; +import publicWidget from "web.public.widget"; + +publicWidget.registry.PortalHomeCounters.include({ + /** + * @override + */ + _getCountersAlwaysDisplayed() { + return this._super(...arguments).concat(["commission_count"]); + }, +}); diff --git a/account_portal_commission/static/src/js/search_encounters.esm.js b/account_portal_commission/static/src/js/search_encounters.esm.js new file mode 100644 index 000000000..ee6d2a52f --- /dev/null +++ b/account_portal_commission/static/src/js/search_encounters.esm.js @@ -0,0 +1,174 @@ +odoo.define("search_encounters.button", function (require) { + const publicWidget = require("web.public.widget"); + const Dialog = require("web.Dialog"); + // Const _t = core._t; + + function fromField() { + const code = document.createElement("input"); + code.setAttribute("name", "code"); + code.setAttribute("class", "form-control col-10 col-md-6"); + code.setAttribute("placeholder", "Internal Identifier"); + code.required = true; + code.maxLength = 9; + code.minLength = 9; + return code; + } + + function fixupView(oldNode) { + let node, + code = null; + switch (oldNode.nodeType) { + case 1: + if (oldNode.tagName === "field") { + node = fromField(); + if (oldNode.getAttribute("name") === "code") { + code = node; + break; + } + break; + } + node = document.createElement(oldNode.tagName); + for (let i = 0; i < oldNode.attributes.length; ++i) { + const attr = oldNode.attributes[i]; + node.setAttribute(attr.name, attr.value); + } + for (let j = 0; j < oldNode.childNodes.length; ++j) { + const [ch, co] = fixupView(oldNode.childNodes[j]); + if (co) { + code = co; + } + if (ch) { + node.appendChild(ch); + } + } + break; + case 3: + case 4: + node = document.createTextNode(oldNode.data); + break; + default: + } + return [node, code]; + } + + class Button { + constructor(parent, id, model, input_node, button_node) { + this._parent = parent; + this.text = button_node.getAttribute("string"); + this.record_id = id; + this.model = model; + this.classes = button_node.getAttribute("class") || null; + this.action = button_node.getAttribute("name"); + this.input = input_node; + if (button_node.getAttribute("special") === "cancel") { + this.close = true; + this.click = null; + } else { + this.close = false; + // Because Dialog doesnt' call() click on the descriptor object + this.click = this._click.bind(this); + } + } + async _click() { + await this.callAction(this.record_id, {code: this.input.value}); + } + async callAction(id, update) { + await this._parent + ._rpc({model: this.model, method: this.action, args: [id, update.code]}) + .then(function (result) { + console.log(result); + if (result.id === false) { + new Dialog(this, { + title: "Encounter Information", + size: "large", + $content: `

No encounter has been found for this internal identifier.

`, + buttons: [ + { + text: "Cancel", + close: true, + }, + ], + }).open(); + return; + } + var contentText = `
+

Internal Identifier: ${_.str.escapeHTML( + result.internal_identifier + )}

+ `; + + if (result.commissions) { + contentText += `

Related commissions: + + `; + for (const commission of result.commissions) { + contentText += ` + + + `; + } + contentText += `
${_.str.escapeHTML( + commission.name + )}${_.str.escapeHTML( + commission.amount + )}€
`; + } + contentText += `

`; + new Dialog(this, { + title: "Encounter Information", + size: "medium", + $content: contentText, + buttons: [ + { + text: "Cancel", + close: true, + }, + ], + }).open(); + }); + } + } + + publicWidget.registry.SearchEncountersButton = publicWidget.Widget.extend({ + selector: "#search_encounters", + events: { + click: "_onClick", + }, + + async _onClick(e) { + e.preventDefault(); + + const w = await this._rpc({ + model: "res.users", + method: "action_open_wizard", + args: [this.getSession().user_id], + }); + + const {res_model: model, res_id: wizard_id} = w; + + const doc = new DOMParser().parseFromString( + document.getElementById("search_encounters_wizard_view").textContent, + "application/xhtml+xml" + ); + + const xmlBody = doc.querySelector("sheet *"); + const [body, code] = fixupView(xmlBody); + + const buttons = []; + for (const button of doc.querySelectorAll("footer button")) { + buttons.push(new Button(this, wizard_id, model, code, button)); + } + + // Wrap in a root host of .modal-body otherwise it breaks our neat flex layout + const $content = document.createElement("form"); + $content.appendChild(body); + // Implicit submission by pressing [return] from within input + $content.addEventListener("submit", (e) => { + e.preventDefault(); + // Sadness: footer not available as normal element + dialog.$footer.find(".btn-primary").click(); + }); + var dialog = new Dialog(this, {$content, buttons}).open(); + }, + }); +}); diff --git a/account_portal_commission/views/account_portal_templates.xml b/account_portal_commission/views/account_portal_templates.xml new file mode 100644 index 000000000..1bd5c502a --- /dev/null +++ b/account_portal_commission/views/account_portal_templates.xml @@ -0,0 +1,212 @@ + + + + + + + + diff --git a/account_portal_commission/wizards/__init__.py b/account_portal_commission/wizards/__init__.py new file mode 100644 index 000000000..410a48752 --- /dev/null +++ b/account_portal_commission/wizards/__init__.py @@ -0,0 +1 @@ +from . import search_encounters_wizard_view diff --git a/account_portal_commission/wizards/search_encounters_wizard_view.py b/account_portal_commission/wizards/search_encounters_wizard_view.py new file mode 100644 index 000000000..d3e31dc88 --- /dev/null +++ b/account_portal_commission/wizards/search_encounters_wizard_view.py @@ -0,0 +1,29 @@ +from odoo import api, fields, models + + +class TOTPWizard(models.TransientModel): + _name = "search_encounters.wizard" + _description = "Search Encounters Wizard" + + code = fields.Char(string="Internal Identifier") + + @api.model + def search_encounter(self, id, code): + + encounter = self.env["medical.encounter"].search( + [("internal_identifier", "=", code)], limit=1 + ) + + return { + "id": encounter.id, + "internal_identifier": encounter.internal_identifier, + "commissions": [ + { + "name": commission.object_id.name, + "amount": commission.amount, + } + for commission in encounter.sale_order_ids.order_line.invoice_lines.agent_ids.filtered( + lambda r: r.agent_id.id == self.env.user.partner_id.id + ) + ], + } diff --git a/account_portal_commission/wizards/search_encounters_wizard_view.xml b/account_portal_commission/wizards/search_encounters_wizard_view.xml new file mode 100644 index 000000000..e711668dd --- /dev/null +++ b/account_portal_commission/wizards/search_encounters_wizard_view.xml @@ -0,0 +1,42 @@ + + + + search_encounters wizard + search_encounters.wizard + +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
diff --git a/setup/account_portal_commission/odoo/addons/account_portal_commission b/setup/account_portal_commission/odoo/addons/account_portal_commission new file mode 120000 index 000000000..5c7f245a8 --- /dev/null +++ b/setup/account_portal_commission/odoo/addons/account_portal_commission @@ -0,0 +1 @@ +../../../../account_portal_commission \ No newline at end of file diff --git a/setup/account_portal_commission/setup.py b/setup/account_portal_commission/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/account_portal_commission/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)