add spreadsheet_dashboard_pos_cooperative & pos_dashboard_cooperative
This commit is contained in:
parent
86101218e9
commit
5247938b86
34 changed files with 7974 additions and 0 deletions
1
pos_dashboard_cooperative/report/__init__.py
Normal file
1
pos_dashboard_cooperative/report/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import pos_order_report
|
||||
130
pos_dashboard_cooperative/report/pos_order_report.py
Normal file
130
pos_dashboard_cooperative/report/pos_order_report.py
Normal 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
|
||||
"""
|
||||
)
|
||||
125
pos_dashboard_cooperative/report/pos_order_report_views.xml
Normal file
125
pos_dashboard_cooperative/report/pos_order_report_views.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue