add spreadsheet_dashboard_pos_cooperative & pos_dashboard_cooperative

This commit is contained in:
Luis 2026-06-05 19:55:00 +02:00
parent 86101218e9
commit 5247938b86
34 changed files with 7974 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import pos_order_report

View file

@ -0,0 +1,130 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class PosOrderReport(models.Model):
_inherit = "report.pos.order"
# --- Time dimensions ----------------------------------------------------
date_day = fields.Date(
string="Order Day",
readonly=True,
)
hour_of_day = fields.Integer(
string="Hour of Day",
readonly=True,
group_operator=None,
)
day_of_week = fields.Integer(
string="Day of Week (1=Mon, 7=Sun)",
readonly=True,
group_operator=None,
)
# --- Category hierarchy -------------------------------------------------
# Family = root category, subfamily = direct parent of categ_id.
# Computed from product.category.parent_path (materialized path
# "1/3/7/"), which is maintained automatically by Odoo (_parent_store).
categ_root_id = fields.Many2one(
"product.category",
string="Product Family (root)",
readonly=True,
)
categ_root_id_int = fields.Integer(
string="Product Family ID (root)",
readonly=True,
group_operator=None,
)
categ_parent_id = fields.Many2one(
"product.category",
string="Product Subfamily (parent)",
readonly=True,
)
categ_parent_id_int = fields.Integer(
string="Product Subfamily ID (parent)",
readonly=True,
group_operator=None,
)
product_categ_id_int = fields.Integer(
string="Product Category ID (leaf)",
readonly=True,
group_operator=None,
)
product_id_int = fields.Integer(
string="Product ID",
readonly=True,
group_operator=None,
)
# --- Cooperative member dimension --------------------------------------
# True when the customer of the ticket held at least one share line whose
# effective_date was on or before the order date, in the same company.
#
# NOTE: this is an approximation. cooperator does not store an end date
# on share.line; share buy-backs and transfers are recorded only in
# subscription.register. A future version may refine the calculation by
# consuming subscription.register to substract sell_back/transfer
# operations, at the cost of a more expensive subquery.
is_member_at_date = fields.Boolean(
string="Member at Order Date",
readonly=True,
)
def _select(self):
return (
super()._select()
+ """,
s.date_order::date AS date_day,
EXTRACT(HOUR FROM (s.date_order AT TIME ZONE 'UTC') AT TIME ZONE 'Europe/Madrid')::int AS hour_of_day,
EXTRACT(ISODOW FROM s.date_order)::int AS day_of_week,
cat_root.id AS categ_root_id,
cat_root.id AS categ_root_id_int,
pt_categ.parent_id AS categ_parent_id,
pt_categ.parent_id AS categ_parent_id_int,
pt.categ_id AS product_categ_id_int,
pt.id AS product_id_int,
CASE WHEN EXISTS (
SELECT 1 FROM share_line sl
WHERE sl.partner_id = s.partner_id
AND sl.company_id = s.company_id
AND sl.effective_date IS NOT NULL
AND sl.effective_date <= s.date_order::date
) THEN TRUE ELSE FALSE END AS is_member_at_date
"""
)
def _from(self):
# Extra joins to resolve:
# - pt_categ: direct category of the product (used for parent_id and
# parent_path lookup).
# - cat_root: root category, found by taking the first id in
# parent_path. parent_path looks like "1/3/7/".
return (
super()._from()
+ """
LEFT JOIN product_category pt_categ ON pt_categ.id = pt.categ_id
LEFT JOIN product_category cat_root
ON cat_root.id = NULLIF(
split_part(pt_categ.parent_path, '/', 1), ''
)::int
"""
)
def _group_by(self):
# Append non-aggregated expressions added in _select() that are not
# already in the parent GROUP BY. s.date_order is already there
# (point_of_sale.report.pos_order_report._group_by), so the time
# dimensions derived from it (date_day, hour_of_day, day_of_week)
# are functionally dependent and don't need to be listed.
# is_member_at_date is a scalar subquery; not needed either.
return (
super()._group_by()
+ """,
cat_root.id,
pt_categ.parent_id,
pt.categ_id,
pt.id
"""
)

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 Criptomart - License AGPL-3 -->
<odoo>
<!--
Extend the core search view to expose:
- Member dimension filters (members / non-members)
- Category hierarchy group-bys (family, subfamily)
- Time dimension group-bys (day of week, hour of day)
- Filters by company (since this module is multi-company aware)
-->
<record id="report_pos_order_view_search" model="ir.ui.view">
<field name="name">report.pos.order.search.cooperative</field>
<field name="model">report.pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_report_pos_order_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='not_invoiced']" position="after">
<separator/>
<filter
string="Members"
name="members"
domain="[('is_member_at_date', '=', True)]"
/>
<filter
string="Non members"
name="non_members"
domain="[('is_member_at_date', '=', False)]"
/>
</xpath>
<xpath expr="//group" position="inside">
<separator/>
<filter
string="Member"
name="group_member"
context="{'group_by': 'is_member_at_date'}"
/>
<filter
string="Product Family"
name="group_categ_root"
context="{'group_by': 'categ_root_id'}"
/>
<filter
string="Product Subfamily"
name="group_categ_parent"
context="{'group_by': 'categ_parent_id'}"
/>
<separator/>
<filter
string="Day of Week"
name="group_day_of_week"
context="{'group_by': 'day_of_week'}"
/>
<filter
string="Hour of Day"
name="group_hour_of_day"
context="{'group_by': 'hour_of_day'}"
/>
</xpath>
</field>
</record>
<!--
Extend the core pivot view to include the most useful measures for
a cooperative supermarket dashboard: price total (sales), margin,
margin rate and order count. Members vs non-members can then be
compared by adding the "Member" group-by from the search bar.
-->
<record id="report_pos_order_view_pivot" model="ir.ui.view">
<field name="name">report.pos.order.pivot.cooperative</field>
<field name="model">report.pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_report_pos_order_pivot"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='price_total']" position="after">
<field name="margin" type="measure"/>
<field name="margin_rate" type="measure"/>
</xpath>
</field>
</record>
<!--
Extend the core tree view to expose the new dimensions as optional
columns so users can toggle them without changing the default UI.
-->
<record id="report_pos_order_view_tree" model="ir.ui.view">
<field name="name">report.pos.order.tree.cooperative</field>
<field name="model">report.pos.order</field>
<field name="inherit_id" ref="point_of_sale.report_pos_order_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_categ_id']" position="after">
<field name="categ_root_id" optional="hide"/>
<field name="categ_parent_id" optional="hide"/>
<field name="is_member_at_date" optional="show"/>
<field name="hour_of_day" optional="hide"/>
<field name="day_of_week" optional="hide"/>
</xpath>
</field>
</record>
<!--
Cooperative-flavoured action over report.pos.order. Defaults to a
graph view grouped by month, with the most relevant measures
pre-selected. Pivot + tree are also available.
-->
<record id="action_pos_dashboard_overview" model="ir.actions.act_window">
<field name="name">Cooperative POS Overview</field>
<field name="res_model">report.pos.order</field>
<field name="view_mode">graph,pivot,tree</field>
<field name="search_view_id"
ref="point_of_sale.view_report_pos_order_search"/>
<field name="context">{
'search_default_order_month': 1,
'group_by_no_leaf': 1,
'group_by': [],
}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No data yet!
</p><p>
Sell some products at the Point of Sale to populate this
cooperative dashboard.
</p>
</field>
</record>
</odoo>