Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bbeeb66
[ADD] estate: create new estate module manifest
Yoann-bary Jan 19, 2026
0e3670e
[IMP] estate: defined estate property model
Yoann-bary Jan 19, 2026
8f57e10
[IMP] estate: added security permissions
Yoann-bary Jan 19, 2026
53017ad
[IMP] estate: add GUI to view and edit properties
Yoann-bary Jan 19, 2026
52cf939
[IMP] estate: create views for estate models
Yoann-bary Jan 19, 2026
7e9b857
[ADD] estate: create relational models describing properties
Yoann-bary Jan 20, 2026
4979e4a
[IMP] estate: add dynamic related field update
Yoann-bary Jan 20, 2026
7dc437e
[IMP] estate: add button handlers for offer status operations
Yoann-bary Jan 20, 2026
36bcd18
[IMP] estate: add constraints on prices and tags
Yoann-bary Jan 20, 2026
2c9ada0
[IMP] estate: beautify add relations to menus
Yoann-bary Jan 20, 2026
4d65f98
[IMP] estate: add user model connectivity
Yoann-bary Jan 21, 2026
345da80
[ADD] estate_account: generate invoices from sold properties
Yoann-bary Jan 21, 2026
1c38baf
[IMP] estate: add kanban view to properties
Yoann-bary Jan 21, 2026
2d76645
[ADD] awesome_owl: add counter and card components to playground
Yoann-bary Jan 21, 2026
3dacb41
[ADD] awesome_owl: add todo_list component
Yoann-bary Jan 21, 2026
01cd97a
[IMP] awesome_owl: enable using templates in card body
Yoann-bary Jan 21, 2026
93b879c
[LINT] estate, awesome_owl: fix code formatting
Yoann-bary Jan 22, 2026
b742565
[ADD] awesome_dashboard: add order statistics dashboard
Yoann-bary Jan 22, 2026
d3085d0
[ADD] awesome_dashboard: add pie charts to dashboard
Yoann-bary Jan 22, 2026
970353c
[IMP] awesome_dashboard: update dashboard in real time
Yoann-bary Jan 22, 2026
0773df9
[IMP] awesome_dashboard: make dashboard generic
Yoann-bary Jan 22, 2026
b43d4b9
[IMP] awesome_dashboard: make dashboard customizable
Yoann-bary Jan 23, 2026
591db8c
[ADD] awesome_clicker: create clicker game interface
Yoann-bary Jan 23, 2026
8780201
[IMP] awesome_clicker: add progression systems and unlocks
Yoann-bary Jan 23, 2026
cbbf0fc
[IMP] awesome_clicker: add random reward pop-ups
Yoann-bary Jan 26, 2026
2db3967
[IMP] awesome_clicker: add tree resources and revamp menus
Yoann-bary Jan 26, 2026
a2e0c00
[IMP] awesome_clicker: add game state persistence
Yoann-bary Jan 26, 2026
3e0d2e4
[ADD] awesome_gallery: create new 'gallery' view type
Yoann-bary Jan 27, 2026
9fb06c8
[IMP] awesome_kanban: define kanban view extension
Yoann-bary Jan 27, 2026
4b07c85
[IMP] awesome_gallery: load partners image data
Yoann-bary Jan 27, 2026
3225bb3
[IMP] estate: add data and demo record
Yoann-bary Jan 28, 2026
3367e17
[FIX] estate: check property sale conditions
Yoann-bary Jan 28, 2026
bafb81d
[ADD] importable_estate: create import module for estate
Yoann-bary Jan 29, 2026
b89c8aa
[REF] awesome_gallery: reorganize gallery view architecure
Yoann-bary Jan 29, 2026
91c5c14
[IMP] awesome_gallery: display images and link form view
Yoann-bary Jan 29, 2026
2c3296f
[IMP] awesome_gallery: add pagination and view validation
Yoann-bary Jan 30, 2026
7c21e68
[IMP] awesome_gallery: add upload image button on records
Yoann-bary Jan 30, 2026
d16f815
[IMP] awesome_kanban: add customer list and filters
Yoann-bary Jan 30, 2026
f3cfa4b
[IMP] awesome_kanban: add filter options to customer list
Yoann-bary Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# VSCode config files
.vscode/
24 changes: 24 additions & 0 deletions awesome_clicker/static/src/click_rewards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const rewards = [
{
description: "Get 1 ClickBot",
apply(clicker) {
clicker.bots.clickbot.quantity++;
},
maxLevel: 3,
},
{
description: "Get 1 BigBot",
apply(clicker) {
clicker.bots.bigbot.quantity++;
},
minLevel: 3,
maxLevel: 4,
},
{
description: "Increase bot power!",
apply(clicker) {
clicker.power++;
},
minLevel: 3,
},
];
17 changes: 17 additions & 0 deletions awesome_clicker/static/src/click_value/click_value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Component } from "@odoo/owl";
import { humanNumber } from "@web/core/utils/numbers";

import { useClicker } from "../clicker_service";


export class ClickValue extends Component {
static template = "awesome_clicker.ClickValue";

setup() {
this.clicker = useClicker();
}

getClickValueDisplay() {
return humanNumber(this.clicker.clicks, { decimals: 1 });
}
}
8 changes: 8 additions & 0 deletions awesome_clicker/static/src/click_value/click_value.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_clicker.ClickValue">
<span t-att-data-tooltip="this.clicker.clicks">
<t t-esc="getClickValueDisplay()"/>
</span>
</t>
</templates>
117 changes: 117 additions & 0 deletions awesome_clicker/static/src/clicker_model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { EventBus } from "@odoo/owl";
import { browser } from "@web/core/browser/browser";
import { Reactive } from "@web/core/utils/reactive";

import { chooseReward } from "./utils";


export const LEVEL_REQUIREMENTS = [
{ level: 1, requirement: 1000, event_name: "MILESTONE_1k", message: "Level up! You have unlocked ClickBots." },
{ level: 2, requirement: 5000, event_name: "MILESTONE_5k", message: "Level up! You have unlocked BigBots." },
{ level: 3, requirement: 100000, event_name: "MILESTONE_100k", message: "Level up! You have unlocked Power." },
{ level: 4, requirement: 1000000, event_name: "MILESTONE_1M", message: "Level up! You have unlocked Trees." },
];

export class ClickerModel extends Reactive {

constructor() {
super();

this.version = "1.1";
this.clicks = 0;
this.level = 0;
this.power = 1;
this.bots = {
clickbot: {
name: "ClickBot",
quantity: 0,
price: 1000,
yield: 10,
level_required: 1,
},
bigbot: {
name: "BigBot",
quantity: 0,
price: 5000,
yield: 100,
level_required: 2,
}
};
this.trees = {
pear: {
name: "Pear Tree",
fruit_name: "Pear",
quantity: 0,
price: 1000000,
fruits: 0,
level_required: 4,
},
cherry: {
name: "Cherry Tree",
fruit_name: "Cherry",
quantity: 0,
price: 1000000,
fruits: 0,
level_required: 4,
},
peach: {
name: "Peach Tree",
fruit_name: "Peach",
quantity: 0,
price: 1000000,
fruits: 0,
level_required: 4,
}
}

this.bus = new EventBus();
document.addEventListener("click", () => this.increment(1), { capture: true });
setInterval(() => {
Object.values(this.bots).forEach(bot => this.clicks += bot.yield * this.power * bot.quantity);
browser.localStorage.setItem("clicker_state", JSON.stringify(this));
}, 10 * 1000);
setInterval(() => {
Object.values(this.trees).forEach(tree => tree.fruits += tree.quantity);
}, 30 * 1000);
}

increment(inc) {
this.clicks += inc;

LEVEL_REQUIREMENTS.forEach(milestone => {
if (this.clicks >= milestone.requirement && this.level < milestone.level) {
this.level++;
this.bus.trigger(milestone.event_name);
}
})
}

purchaseBot(bot_name) {
let purchased_bot = Object.values(this.bots).find(bot => bot.name === bot_name);
purchased_bot.quantity++;
this.clicks -= purchased_bot.price;
}

purchaseTree(tree_name) {
let purchased_tree = Object.values(this.trees).find(tree => tree.name === tree_name);
purchased_tree.quantity++;
this.clicks -= purchased_tree.price;
}

purchasePower() {
this.power++;
this.clicks -= 50000;
}

getReward() {
this.bus.trigger("RANDOM_REWARD", chooseReward(this.level));
}

getTotalTreeCount() {
return Object.values(this.trees).map(tree => tree.quantity).reduce((sum, qty) => { sum + qty }, 0);
}

getTotalFruitCount() {
return Object.values(this.trees).map(tree => tree.fruits).reduce((sum, qty) => { sum + qty }, 0);
}
}
72 changes: 72 additions & 0 deletions awesome_clicker/static/src/clicker_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useState } from "@odoo/owl";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";

import { ClickerModel, LEVEL_REQUIREMENTS } from "./clicker_model";
import { migrate } from "./migration";


function initClickerState() {
let clicker_model = new ClickerModel();
let local_state = JSON.parse(browser.localStorage.getItem("clicker_state"));

if (!local_state) {
return clicker_model;
}

if (local_state.version != clicker_model.version) {
migrate(local_state, clicker_model.version);
}

delete local_state.bus;
return Object.assign(clicker_model, local_state);
}

const clickerService = {
dependencies: ["action", "effect", "notification"],
start(env, services) {
let clicker_model = initClickerState();

LEVEL_REQUIREMENTS.forEach(milestone =>
clicker_model.bus.addEventListener(
milestone.event_name,
() => services.effect.add({ message: milestone.message }),
)
)

clicker_model.bus.addEventListener(
"RANDOM_REWARD",
(ev) => {
const closeNotification = services.notification.add(
`Congratulations, you won a reward: '${ev.detail.description}'`,
{
type: "success",
sticky: true,
buttons: [{
name: "Collect",
onClick: () => {
ev.detail.apply(clicker_model);
closeNotification();
services.action.doAction({
type: "ir.actions.client",
tag: "awesome_clicker.client_action",
target: "new",
name: "Clicker"
});
}
}],
}
)
}
)

return clicker_model;
}
}

export function useClicker() {
return useState(useService("awesome_clicker.clicker_service"));
}

registry.category("services").add("awesome_clicker.clicker_service", clickerService);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Component } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";

import { useClicker } from "../clicker_service";
import { ClickValue } from "../click_value/click_value";


export class ClickerSystrayItem extends Component {
static template = "awesome_clicker.ClickerSystrayItem";
static components = { ClickValue, Dropdown, DropdownItem };

setup() {
this.clicker = useClicker();
this.action_service = useService("action");
}

openClickerWindow() {
this.action_service.doAction({
type: "ir.actions.client",
tag: "awesome_clicker.client_action",
target: "new",
name: "Clicker"
});
}
}

registry.category("systray").add("awesome_clicker.ClickerSystrayItem", { Component: ClickerSystrayItem });
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_clicker.ClickerSystrayItem">
<Dropdown>
<button>
<b><ClickValue/><div class="ms-1 fa fa-mouse-pointer"/></b>
<div t-if="this.clicker.level >= 4">
<b>, <t t-esc="this.clicker.getTotalTreeCount()"/> <div class="ms-1 fa fa-tree"/></b>
<b>, <t t-esc="this.clicker.getTotalFruitCount()"/> <div class="ms-1 fa fa-apple"/></b>
</div>
</button>

<t t-set-slot="content">
<DropdownItem>
<button class="btn btn-primary" t-on-click="this.openClickerWindow">Open Clicker Game</button>
</DropdownItem>
<DropdownItem>
<button class="btn btn-primary" t-on-click="() => this.clicker.purchaseBot('ClickBot')">Buy ClickBot</button>
</DropdownItem>

<t t-foreach="Object.values(this.clicker.trees)" t-as="tree" t-key="tree.name">
<DropdownItem t-if="this.clicker.level >= tree.level_required">
<t t-esc="tree.quantity"/>x <t t-esc="tree.name"/>
(<t t-esc="tree.fruits"/> <t t-esc="tree.fruit_name"/>)
</DropdownItem>
</t>
</t>
</Dropdown>
</t>
</templates>
18 changes: 18 additions & 0 deletions awesome_clicker/static/src/client_action/client_action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Component } from "@odoo/owl";
import { Notebook } from "@web/core/notebook/notebook";
import { registry } from "@web/core/registry";

import { useClicker } from "../clicker_service";
import { ClickValue } from "../click_value/click_value";


export class ClientAction extends Component {
static template = "awesome_clicker.ClientAction";
static components = { ClickValue, Notebook };

setup() {
this.clicker = useClicker();
}
}

registry.category("actions").add("awesome_clicker.client_action", ClientAction);
50 changes: 50 additions & 0 deletions awesome_clicker/static/src/client_action/client_action.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_clicker.ClientAction">
<div class="p-3">
<div class="m-2 d-inline-block">
<b>Clicks: <ClickValue/></b>
<button class="btn btn-secondary fa fa-plus ms-1" t-on-click="() => this.clicker.increment(9)"/>
</div>

<Notebook>
<t t-set-slot="page_bots" title="'Clicks'" isVisible="this.clicker.level >= 1">
<h1>Bots</h1>
<t t-foreach="Object.values(this.clicker.bots)" t-as="bot" t-key="bot.name">
<div class="m-2" t-if="this.clicker.level >= bot.level_required">
<a><t t-esc="bot.quantity"/>x <t t-esc="bot.name"/> (<t t-esc="bot.yield"/> clicks/10s)</a>
<button class="btn btn-primary ms-1" t-on-click="() => this.clicker.purchaseBot(bot.name)" t-att-disabled="this.clicker.clicks &lt; bot.price">
Buy <t t-esc="bot.name"/> (<t t-esc="bot.price"/>)
</button>
</div>
</t>

<div class="m-2" t-if="this.clicker.level >= 3">
<a><t t-esc="this.clicker.power"/>x multiplier to all bot clicks</a>
<button class="btn btn-primary ms-1" t-on-click="() => this.clicker.purchasePower()" t-att-disabled="this.clicker.clicks &lt; 50000">
Buy power (50000)
</button>
</div>
</t>

<t t-set-slot="page_trees" title="'Trees'" isVisible="this.clicker.level >= 4">
<h1>Fruits</h1>
<t t-foreach="Object.values(this.clicker.trees)" t-as="tree" t-key="tree.name">
<b class="ms-2" t-if="this.clicker.level >= tree.level_required">
<t t-esc="tree.fruits"/>x <t t-esc="tree.fruit_name"/>
</b>
</t>
<h1>Trees</h1>
<t t-foreach="Object.values(this.clicker.trees)" t-as="tree" t-key="tree.name">
<div class="m-2" t-if="this.clicker.level >= tree.level_required">
<a><t t-esc="tree.quantity"/>x <t t-esc="tree.name"/> (1 <t t-esc="tree.fruit_name"/>/30s)</a>
<button class="btn btn-primary ms-1" t-on-click="() => this.clicker.purchaseTree(tree.name)" t-att-disabled="this.clicker.clicks &lt; tree.price">
Buy <t t-esc="tree.name"/> (<t t-esc="tree.price"/>)
</button>
</div>
</t>
</t>
</Notebook>
</div>
</t>
</templates>
Loading