215 lines
8.1 KiB
Python
215 lines
8.1 KiB
Python
# Copyright 2026 Criptomart
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
from datetime import date, timedelta
|
|
|
|
import odoo
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
|
|
@odoo.tests.tagged("post_install", "-at_install")
|
|
class TestPosDashboardCooperative(TransactionCase):
|
|
"""Validate the extra dimensions added by pos_dashboard_cooperative on
|
|
top of point_of_sale.report.pos.order:
|
|
|
|
- time dimensions (date_day, hour_of_day, day_of_week)
|
|
- product category hierarchy (categ_root_id, categ_parent_id)
|
|
- cooperative member historic flag (is_member_at_date)
|
|
|
|
These tests use a minimal hand-built pos.session to stay independent
|
|
from any existing data in the database and from the heavy
|
|
TestPoSCommon setup machinery, which requires a clean accounting
|
|
setup.
|
|
"""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.company = cls.env.ref("base.main_company")
|
|
|
|
# Two-level category hierarchy: family -> subfamily -> leaf
|
|
cls.family_root = cls.env["product.category"].create(
|
|
{"name": "DASHBOARD-FAMILY-ROOT"}
|
|
)
|
|
cls.subfamily = cls.env["product.category"].create(
|
|
{"name": "DASHBOARD-SUBFAMILY", "parent_id": cls.family_root.id}
|
|
)
|
|
cls.categ_leaf = cls.env["product.category"].create(
|
|
{"name": "DASHBOARD-LEAF", "parent_id": cls.subfamily.id}
|
|
)
|
|
|
|
# Product sold at the POS in every test.
|
|
cls.product = cls.env["product.product"].create(
|
|
{
|
|
"name": "DASHBOARD-PRODUCT",
|
|
"type": "consu",
|
|
"available_in_pos": True,
|
|
"list_price": 10.0,
|
|
"standard_price": 6.0,
|
|
"categ_id": cls.categ_leaf.id,
|
|
}
|
|
)
|
|
|
|
# Share product needed by cooperator.share.line.
|
|
cls.share_product = cls.env["product.product"].create(
|
|
{
|
|
"name": "DASHBOARD-SHARE",
|
|
"is_share": True,
|
|
"type": "service",
|
|
"list_price": 25.0,
|
|
}
|
|
)
|
|
|
|
# Two partners: one cooperative member (share dated in the past)
|
|
# and one non-member.
|
|
cls.member_partner = cls.env["res.partner"].create(
|
|
{"name": "DASHBOARD-MEMBER"}
|
|
)
|
|
cls.non_member_partner = cls.env["res.partner"].create(
|
|
{"name": "DASHBOARD-NON-MEMBER"}
|
|
)
|
|
cls.env["share.line"].create(
|
|
{
|
|
"partner_id": cls.member_partner.id,
|
|
"share_product_id": cls.share_product.id,
|
|
"share_number": 1,
|
|
"share_unit_price": 25.0,
|
|
"effective_date": date.today() - timedelta(days=365),
|
|
"company_id": cls.company.id,
|
|
}
|
|
)
|
|
|
|
# Reuse an existing POS config to avoid bootstrapping payment
|
|
# methods and journals from scratch. Any installed config is
|
|
# acceptable since we never go through the POS UI.
|
|
cls.pos_config = cls.env["pos.config"].search(
|
|
[("company_id", "=", cls.company.id)], limit=1
|
|
)
|
|
assert cls.pos_config, "A pos.config in the main company is required."
|
|
if not cls.pos_config.current_session_id:
|
|
cls.pos_config.open_ui()
|
|
cls.pos_session = cls.pos_config.current_session_id
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
def _create_pos_order(self, partner):
|
|
return self.env["pos.order"].create(
|
|
{
|
|
"session_id": self.pos_session.id,
|
|
"partner_id": partner.id,
|
|
"company_id": self.company.id,
|
|
"lines": [
|
|
(
|
|
0,
|
|
0,
|
|
{
|
|
"name": "DASHBOARD-OL",
|
|
"product_id": self.product.id,
|
|
"price_unit": 10.0,
|
|
"discount": 0,
|
|
"qty": 1.0,
|
|
"price_subtotal": 10.0,
|
|
"price_subtotal_incl": 10.0,
|
|
"total_cost": 6.0,
|
|
},
|
|
)
|
|
],
|
|
"amount_total": 10.0,
|
|
"amount_tax": 0.0,
|
|
"amount_paid": 0.0,
|
|
"amount_return": 0.0,
|
|
}
|
|
)
|
|
|
|
def _reports_for(self, partner):
|
|
return (
|
|
self.env["report.pos.order"]
|
|
.sudo()
|
|
.search(
|
|
[
|
|
("product_id", "=", self.product.id),
|
|
("partner_id", "=", partner.id),
|
|
],
|
|
order="id",
|
|
)
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Tests
|
|
# ------------------------------------------------------------------
|
|
def test_member_flag_for_member_partner(self):
|
|
"""Partner with a share line predating the order is flagged as
|
|
member."""
|
|
self._create_pos_order(self.member_partner)
|
|
reports = self._reports_for(self.member_partner)
|
|
self.assertTrue(reports)
|
|
self.assertTrue(reports[0].is_member_at_date)
|
|
|
|
def test_member_flag_for_non_member_partner(self):
|
|
"""Partner without any share line is not flagged as member."""
|
|
self._create_pos_order(self.non_member_partner)
|
|
reports = self._reports_for(self.non_member_partner)
|
|
self.assertTrue(reports)
|
|
self.assertFalse(reports[0].is_member_at_date)
|
|
|
|
def test_member_flag_is_historic(self):
|
|
"""Share line dated AFTER the order date does not mark the partner
|
|
as member for that order."""
|
|
future_member = self.env["res.partner"].create(
|
|
{"name": "DASHBOARD-FUTURE-MEMBER"}
|
|
)
|
|
self._create_pos_order(future_member)
|
|
self.env["share.line"].create(
|
|
{
|
|
"partner_id": future_member.id,
|
|
"share_product_id": self.share_product.id,
|
|
"share_number": 1,
|
|
"share_unit_price": 25.0,
|
|
"effective_date": date.today() + timedelta(days=30),
|
|
"company_id": self.company.id,
|
|
}
|
|
)
|
|
reports = self._reports_for(future_member)
|
|
self.assertTrue(reports)
|
|
self.assertFalse(
|
|
reports[0].is_member_at_date,
|
|
"Member flag must reflect the situation at order date.",
|
|
)
|
|
|
|
def test_category_hierarchy_is_resolved(self):
|
|
"""categ_root_id and categ_parent_id reflect the product category
|
|
ancestors derived from parent_path."""
|
|
self._create_pos_order(self.member_partner)
|
|
reports = self._reports_for(self.member_partner)
|
|
self.assertTrue(reports)
|
|
report = reports[0]
|
|
self.assertEqual(report.categ_root_id, self.family_root)
|
|
self.assertEqual(report.categ_parent_id, self.subfamily)
|
|
|
|
def test_time_dimensions_are_populated(self):
|
|
"""date_day matches order date; hour_of_day / day_of_week are in
|
|
valid ranges."""
|
|
self._create_pos_order(self.member_partner)
|
|
reports = self._reports_for(self.member_partner)
|
|
self.assertTrue(reports)
|
|
report = reports[0]
|
|
self.assertIsNotNone(report.date_day)
|
|
self.assertGreaterEqual(report.hour_of_day, 0)
|
|
self.assertLess(report.hour_of_day, 24)
|
|
self.assertGreaterEqual(report.day_of_week, 1)
|
|
self.assertLessEqual(report.day_of_week, 7)
|
|
|
|
def test_read_group_by_member_flag(self):
|
|
"""Aggregations grouped by the member flag return one row per
|
|
member status when both kinds of partners have purchased."""
|
|
self._create_pos_order(self.member_partner)
|
|
self._create_pos_order(self.non_member_partner)
|
|
results = self.env["report.pos.order"].read_group(
|
|
[("product_id", "=", self.product.id)],
|
|
["price_total", "is_member_at_date"],
|
|
["is_member_at_date"],
|
|
)
|
|
statuses = {r["is_member_at_date"] for r in results}
|
|
self.assertIn(True, statuses)
|
|
self.assertIn(False, statuses)
|