Skip to content

[ADD] estate and awesome dashboard: add estate module for property selling and build awesome_dashboard using owl #813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: 18.0
Choose a base branch
from
Draft
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion awesome_dashboard/__manifest__.py
Original file line number Diff line number Diff line change
@@ -23,7 +23,10 @@
],
'assets': {
'web.assets_backend': [
'awesome_dashboard/static/src/**/*',
'awesome_dashboard/static/src/lazy_load_wrapper.js',
],
'awesome_dashboard.dashboard': [
'awesome_dashboard/static/src/dashboard/**/*',
],
},
'license': 'AGPL-3'
10 changes: 0 additions & 10 deletions awesome_dashboard/static/src/dashboard.js

This file was deleted.

8 changes: 0 additions & 8 deletions awesome_dashboard/static/src/dashboard.xml

This file was deleted.

47 changes: 47 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/** @odoo-module **/

import { Component, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
import { DashboardItem } from "./dashboard_item";
import { Piechart } from "./piechart";
import { registry } from "@web/core/registry";
import { DBModal } from "./dashboard_setting_modal";

export class AwesomeDashboard extends Component {
static template = "awesome_dashboard.AwesomeDashboard";

static components = { Layout, DashboardItem, Piechart };

setup() {
this.action = useService("action");
this.statisticService = useService("load_statistics");
this.data = useState(this.statisticService);
this.dialog = useService("dialog");
}

openMyModal() {
this.dialog.add(DBModal, {
items: this.data.stats,
chart: this.data.chartData,
});
}

viewCustomers() {
this.action.doAction("base.action_partner_form");
}

viewLeads() {
this.action.doAction({
type: "ir.actions.act_window",
target: "current",
res_model: "crm.lead",
views: [
[false, "form"],
[false, "list"],
],
});
}
}

registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
6 changes: 6 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.o_dashboard {
background-color: #111827;
.db-item-title {
font-size:18px;
}
}
26 changes: 26 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
<Layout className="'o_dashboard h-100'" display="{ controlPanel: {} }">
<t t-set-slot="control-panel-create-button">
<button class="btn btn-primary" t-on-click="viewCustomers">Customers</button>
<button class="btn btn-primary ms-2" t-on-click="viewLeads">Leads</button>
<span class="fa fa-gear cursor-pointer mt-2 mx-2" t-on-click="openMyModal"/>
</t>
<div class="flex-wrap d-flex gap-3 p-3">
<t t-if="this.data.stats.length > 0">
<t t-foreach="this.data.stats" t-as="stat" t-key="stat.title">
<t t-if="stat.isVisible">
<DashboardItem title="stat.title" value="stat.value" size="stat.size"/>
</t>
</t>
<t t-if="this.data.chartData.isVisible">
<Piechart chartData="this.data.chartData"/>
</t>
</t>
</div>
</Layout>
</t>

</templates>
15 changes: 15 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @odoo-module **/

import { Component } from "@odoo/owl";

export class DashboardItem extends Component {
static template = "awesome_dashboard.dashboard_item";

static components = {};

static props = ["size", "title", "value"];

static defaultProps = {
size: 1,
};
}
11 changes: 11 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.dashboard_item">
<div t-att-style="`width:${18*props.size}rem`" class="bg-light p-3 rounded">
<p t-out="props.title" class="db-item-title"/>
<h1 t-out="props.value" class="text-success"/>
</div>
</t>

</templates>
46 changes: 46 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_setting_modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component } from "@odoo/owl";
import { Dialog } from "@web/core/dialog/dialog";

export class DBModal extends Component {
static template = "awesome_dashboard.db_modal";
static components = { Dialog };

static props = ['items','chart']

setup() {
this.items = this.props.items;
this.chart = this.props.chart;
this.visibleList = this.items.reduce((acc, crr) => {
if (crr?.isVisible) {
acc?.push(crr?.id);
}
return acc;
}, []);

if (this.chart.isVisible) {
this.visibleList.push("chart");
}
}

handleItemToggle = (_, id) => {
if (this.visibleList.includes(id)) {
this.visibleList = this.visibleList.filter((i) => i !== id);
} else {
this.visibleList.push(id);
}
};

handleApplySetting() {
this.items.forEach((item) => {
item.isVisible = this.visibleList.includes(item?.id);
});

this.chart.isVisible = this.visibleList.includes("chart");

localStorage.setItem(
"dashboardItemVisibility",
JSON.stringify(this.visibleList)
);
this.props.close();
}
}
21 changes: 21 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_setting_modal.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_dashboard.db_modal">
<Dialog title="'Dashboard items configuration'">
<h4>Which cards do you wish to see ?</h4>
<t t-foreach="this.items" t-as="item" t-key="item.title">
<div class="d-flex align-items-center mb-1">
<input type="checkbox" t-on-change="(e)=>handleItemToggle(e,item.id)" t-att-checked="item.isVisible"/>
<p class="mb-0 ms-2" t-out="item.title"/>
</div>
</t>
<div class="d-flex align-items-center mb-1">
<input type="checkbox" t-on-change="(e)=>handleItemToggle(e,'chart')" t-att-checked="this.chart.isVisible"/>
<p class="mb-0 ms-2">T-shirts Sales by size</p>
</div>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="handleApplySetting">Apply</button>
</t>
</Dialog>
</t>
</templates>
59 changes: 59 additions & 0 deletions awesome_dashboard/static/src/dashboard/piechart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, onWillStart, useRef, useEffect } from "@odoo/owl";
import { loadJS } from "@web/core/assets";

export class Piechart extends Component {
static template = "awesome_dashboard.piechart";

static components = {};

static props = ["chartData"];

setup() {
this.canvasRef = useRef("canvas");
this.chart = null;
this.chartData = this.props.chartData;
onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"]));
useEffect(
() => this.renderChart(),
() => [this.chartData]
);
}

renderChart() {
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(this.canvasRef.el, {
data: this.chartData,
type: "pie",
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
labels: {
padding: 20,
usePointStyle: true,
pointStyle: "rect",
font: {
size: 16,
weight: "bold",
color: "#fff",
},
},
},
title: {
display: true,
text: "T-Shirt Sales by Size",
font: {
size: 16,
weight: "bold",
},
padding: 0,
},
},
},
});
}
}
10 changes: 10 additions & 0 deletions awesome_dashboard/static/src/dashboard/piechart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.piechart">
<div class="bg-light p-3 rounded">
<canvas t-ref="canvas"/>
</div>
</t>

</templates>
62 changes: 62 additions & 0 deletions awesome_dashboard/static/src/dashboard/statistic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { reactive } from "@odoo/owl";

const statsMap = {
average_quantity: { id: 1, title: "Average quanitity order" },
average_time: { id: 2, title: "Average time for order from new to sent" },
nb_cancelled_orders: { id: 3, title: "Number of cancelled order this month" },
nb_new_orders: { id: 4, title: "Number of new orders this month" },
total_amount: { id: 5, title: "Total amount of new orders" },
};

const statisticService = {
start() {
let stats = reactive([]);
let chartData = reactive({});
const loadStatistics = async () => {
const result = await rpc("/awesome_dashboard/statistics");
const dbItemVisibility = localStorage.getItem("dashboardItemVisibility");
let formatedres = Object.entries(result).reduce((prev, [key, value]) => {
const item = statsMap[key];

if (item) {
prev.push({
id: item?.id,
title: item?.title,
size: item?.title?.length > 30 ? 2 : 1,
value,
isVisible: dbItemVisibility
? dbItemVisibility.includes(item?.id)
: true,
});
} else if (typeof value === "object") {
chartData.labels = Object.keys(value);
chartData.datasets = [
{
label: "Order by size",
data: Object.values(value),
},
];
chartData.isVisible = dbItemVisibility
? dbItemVisibility.includes("chart")
: true;
}
return prev;
}, []);

stats?.push(...formatedres);

return { stats, chartData };
};

loadStatistics();

return {
stats,
chartData,
};
},
};

registry.category("services").add("load_statistics", statisticService);
16 changes: 16 additions & 0 deletions awesome_dashboard/static/src/lazy_load_wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @odoo-module **/

import { registry } from "@web/core/registry";
import { LazyComponent } from "@web/core/assets";
import { xml, Component } from "@odoo/owl";

class DashoboardLazyLoader extends Component {
static components = { LazyComponent };
static template = xml`
<LazyComponent bundle="'awesome_dashboard.dashboard'" Component="'AwesomeDashboard'" />
`;
}

registry
.category("actions")
.add("awesome_dashboard.dashboard", DashoboardLazyLoader);
15 changes: 15 additions & 0 deletions awesome_owl/static/src/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component , useState } from "@odoo/owl";

export class Card extends Component {
static template = "awesome_owl.card";

static components = { }

setup(){
this.showCardContent = useState({value:true});
}

toggleCardContent(){
this.showCardContent.value = !this.showCardContent.value;
}
}
20 changes: 20 additions & 0 deletions awesome_owl/static/src/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">

<t t-name="awesome_owl.card">
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="header-title mb-0">
<t t-slot="card-title"/>
</h4>
<span class="fa cursor-pointer" t-att-class="showCardContent.value ? 'fa-chevron-up': 'fa-chevron-down'" t-on-click="toggleCardContent"></span>
</div>

<div class="card-body" t-att-class="showCardContent.value ? 'd-flex' : 'd-none'">
<t t-slot="card-content"/>
</div>

</div>
</t>

</templates>
18 changes: 18 additions & 0 deletions awesome_owl/static/src/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** @odoo-module **/

import { Component , useState } from "@odoo/owl";

export class Counter extends Component {
static template = "awesome_owl.counter";

static props = ['btnIndex','onchange']

setup(){
this.count = useState({value:0});
}

do_maths(){
this.count.value++;
this.props.onchange();
}
}
11 changes: 11 additions & 0 deletions awesome_owl/static/src/counter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="awesome_owl.counter">
<div class="p-3">
<p class="mb-1 ms-1 h5">Counter<t t-out="props.btnIndex"/>: <span class="h4 ms-2"><t t-out="count.value"/></span></p>
<button class="btn border bg-primary text-white" t-on-click="do_maths">
Increment
</button>
</div>
</t>
</templates>
15 changes: 14 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
/** @odoo-module **/

import { Component } from "@odoo/owl";
import { Component , useState } from "@odoo/owl";
import { Counter } from "./counter";
import { TodoList } from "./todo/todo_list";
import { Card } from "./card";

export class Playground extends Component {
static template = "awesome_owl.playground";

static components = { Counter ,TodoList , Card}

setup(){
this.sum = useState({value:0})
}

increment(){
this.sum.value++;
}
}
22 changes: 20 additions & 2 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary changes.

<templates xml:space="preserve">

<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<h1>Sum is <t t-out="sum.value"/></h1>

<Card>
<t t-set-slot="card-title">
Counters
</t>
<t t-set-slot="card-content">
<Counter btnIndex="1" onchange.bind="increment"/>
<Counter btnIndex="2" onchange.bind="increment"/>
</t>
</Card>
<Card>
<t t-set-slot="card-title">
Todo list
</t>
<t t-set-slot="card-content">
<TodoList/>
</t>
</Card>
</div>
</t>

8 changes: 8 additions & 0 deletions awesome_owl/static/src/todo/todo_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component , useState } from "@odoo/owl";

export class TodoItem extends Component {
static template = "awesome_owl.todo_item";

static props = ['todo','toggleState','removeItem']

}
11 changes: 11 additions & 0 deletions awesome_owl/static/src/todo/todo_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_owl.todo_item">
<div class="border rounded p-3 d-flex align-items-center gap-2 mt-2" style="font-size:24px;">
<input type="checkbox" class="cursor-pointer" t-att-checked="props.todo.isCompleted" t-on-change="()=> props.toggleState(props.todo.id)"/>
<b t-out="props.todo.id + '.'" />
<p class="mb-0" t-att-class="props.todo.isCompleted ? 'text-muted text-decoration-line-through':''" t-out="props.todo.description"/>
<span class="fa fa-remove text-danger ms-3 cursor-pointer" t-on-click="()=> props.removeItem(props.todo.id)"/>
</div>
</t>
</templates>
53 changes: 53 additions & 0 deletions awesome_owl/static/src/todo/todo_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Component , useState , useRef , onMounted } from "@odoo/owl";
import { TodoItem } from "./todo_item";

function useAutoFocus(refName){
let inputRef = useRef(refName);
onMounted(()=>{
inputRef.el.focus()
})
}

export class TodoList extends Component {
static template = "awesome_owl.todo_list";

static components = { TodoItem }

setup(){
this.todos = useState([]);
this.idCount = 1;

useAutoFocus('todo_input');
}

addTodo(e){
const value = e.target.value;
if(value){
if(e.key === "Enter"){

this.todos.unshift({
id:this.idCount++,
description:value,
isCompleted:false
});

e.target.value = ""
}
}

}

removeItem(id){
const itmIndex = this.todos.findIndex( itm => itm.id == id);
if(itmIndex+1){
this.todos.splice(itmIndex,1);
}
}

toggleState(id){
const item = this.todos.find( x => x.id == id);
if(item){
item.isCompleted = !item.isCompleted;
}
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/todo/todo_list.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="awesome_owl.todo_list">
<div class="p-3 w-100">
<h3 class="mb-4 text-center">Todo items</h3>
<input t-ref="todo_input" placeholder="Enter a new task" class="w-100 p-2 rounded border border-success" t-on-keyup="addTodo"/>
<t t-foreach="todos" t-as="todo" t-key="todo.id">
<TodoItem todo="todo" toggleState.bind="toggleState" removeItem.bind="removeItem"/>
</t>
</div>
</t>
</templates>
1 change: 1 addition & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "Real Estate",
"depends": ["base"],
"license": "LGPL-3",
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"security/estate_property_rules.xml",
"views/estate_property_views.xml",
"views/estate_menus_views.xml",
"views/estate_offers_views.xml",
"views/estate_property_type_views.xml",
"views/res_users_views.xml",
"data/estate_property_type_data.xml",
"data/estate_property_data.xml",
"data/estate_property_offer_data.xml",
"report/estate_property_offers_template.xml",
"report/estate_property_reports.xml",
],
"category": "Real Estate/Brokerage",
"application": True,
}
77 changes: 77 additions & 0 deletions estate/data/estate_property_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">

<record id="estate_property_villa" model="estate.property">
<field name="name">Big Villa</field>
<field name="state">new</field>
<field name="description">A nice and big villa</field>
<field name="postcode">12345</field>
<field name="date_availability">2020-02-02</field>
<field name="expected_price" eval="1600000.0" />
<field name="property_type_id" ref="estate.estate_property_type_residential" />
<field name="selling_price" eval="0.0" />
<field name="bedrooms" eval="6" />
<field name="living_area" eval="100" />
<field name="facades" eval="4" />
<field name="garage" eval="True" />
<field name="garden" eval="True" />
<field name="garden_area" eval="100000" />
<field name="garden_orientation">south</field>
</record>

<record id="estate_property_home" model="estate.property">
<field name="name">Trailer home</field>
<field name="state">cancelled</field>
<field name="description">Home in trailer park</field>
<field name="postcode">54321</field>
<field name="date_availability">2001-01-01</field>
<field name="expected_price" eval="100000.0" />
<field name="selling_price" eval="120000.0" />
<field name="property_type_id" ref="estate.estate_property_type_residential" />
<field name="bedrooms" eval="1" />
<field name="living_area" eval="10" />
<field name="facades" eval="4" />
<field name="garage" eval="False" />
<field name="garden" eval="False" />
<field name="garden_area" eval="0" />
<field name="garden_orientation" eval="False" />
</record>

<record id="estate_property_hut" model="estate.property">
<field name="name">Cozy hut</field>
<field name="state">new</field>
<field name="description">Home in desert</field>
<field name="postcode">54321</field>
<field name="date_availability">2001-01-01</field>
<field name="expected_price" eval="100000.0" />
<field name="selling_price" eval="0.0" />
<field name="property_type_id" ref="estate.estate_property_type_residential" />
<field name="bedrooms" eval="1" />
<field name="living_area" eval="10" />
<field name="facades" eval="4" />
<field name="garage" eval="False" />
<field name="garden" eval="False" />
<field name="garden_area" eval="0" />
<field name="garden_orientation" eval="False" />
<field name="offer_ids"
eval="[
Command.create({
'partner_id': ref('base.res_partner_2'),
'price': 2000000,
'validity': 21
}),
Command.create({
'partner_id': ref('base.res_partner_1'),
'price': 2150000,
'validity': 14
}),
Command.create({
'partner_id': ref('base.res_partner_2'),
'price': 2300000,
'validity': 14
})
]" />
</record>
</data>
</odoo>
28 changes: 28 additions & 0 deletions estate/data/estate_property_offer_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="estate_offer_1" model="estate.property.offer">
<field name="partner_id" ref="base.res_partner_2" />
<field name="property_id" ref="estate.estate_property_villa" />
<field name="price" eval="10000.0" />
<field name="validity" eval="14" />
</record>

<record id="estate_offer_2" model="estate.property.offer">
<field name="partner_id" ref="base.res_partner_2" />
<field name="property_id" ref="estate.estate_property_villa" />
<field name="price" eval="1500000.0" />
<field name="validity" eval="14" />
</record>

<record id="estate_offer_3" model="estate.property.offer">
<field name="partner_id" ref="base.res_partner_3" />
<field name="property_id" ref="estate.estate_property_villa" />
<field name="price" eval="1500001.0" />
<field name="validity" eval="14" />
</record>

<function name="accept_offer" model="estate.property.offer">
<value eval="ref('estate_offer_3')" />
</function>
</odoo>
22 changes: 22 additions & 0 deletions estate/data/estate_property_type_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">

<record id="estate_property_type_residential" model="estate.property.type">
<field name="name">Residential</field>
</record>

<record id="estate_property_type_commercial" model="estate.property.type">
<field name="name">Commercial</field>
</record>

<record id="estate_property_type_industrial" model="estate.property.type">
<field name="name">Industrial</field>
</record>

<record id="estate_property_type_land" model="estate.property.type">
<field name="name">Land</field>
</record>

</data>
</odoo>
5 changes: 5 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import inherited_estate
130 changes: 130 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError


class Property(models.Model):
_name = "estate.property"
_description = "Estate Properties"
_sql_constraints = [
(
"check_expected_price",
"CHECK(expected_price > 0)",
"The expected price of a property must be positive.",
),
(
"check_selling_price",
"CHECK(selling_price >= 0)",
"The selling price of a property must be positive.",
),
]
_order = "id desc"

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
default=fields.Date.today() + relativedelta(months=3), copy=False
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
active = fields.Boolean(default=True)
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
salesman = fields.Many2one("res.users", default=lambda self: self.env.user)
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
buyer = fields.Many2one("res.partner", copy=False)
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id")
state = fields.Selection(
string="Status",
selection=[
("new", "New"),
("offer_received", "Offer received"),
("offer_accepted", "Offer accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
required=True,
copy=False,
default="new",
)
garden_orientation = fields.Selection(
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
help="This is Garden orientation described in directions",
)
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(string="Best offer", compute="_compute_best_price")

@api.depends("garden_area", "living_area")
def _compute_total_area(self):
for prp in self:
prp.total_area = prp.garden_area + prp.living_area
Comment on lines +74 to +77

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is good pratice to define function after declaration of all fields.


@api.depends("offer_ids.price")
def _compute_best_price(self):
for prp in self:
pricelist = prp.mapped("offer_ids.price")
if len(pricelist) > 0:
prp.best_price = max(pricelist)
else:
prp.best_price = 0

@api.onchange("garden")
def _onchange_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = 0
self.garden_orientation = ""

def set_sold_state(self):
if self.state != "cancelled":
if self.state == "offer_accepted":
self.state = "sold"
else:
raise UserError("Only Accepeted offers property can be sold")
else:
raise UserError("Cancelled property can not be sold.")
return True

def set_cancelled_state(self):
if self.state != "sold":
self.state = "cancelled"
else:
raise UserError("Sold property can not be cancelled.")
return True

@api.constrains("selling_price", "expected_price")
def _check_selling_price(self):
for record in self:
if (
record.selling_price
and record.expected_price
and record.selling_price < record.expected_price * 0.9
):
raise ValidationError(
"The selling_price cannot be lower than 90% of the expected price"
)

@api.ondelete(at_uninstall=False)
def _unlink_property(self):
for rcd in self:
if rcd.state not in ("new", "cancelled"):
raise UserError("Only New and Cancelled property can be deleted.")
78 changes: 78 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"
_sql_constraints = [
(
"check_offer_price",
"CHECK(price > 0)",
"The Offer price of a property must be positive.",
),
]
_order = "price desc"

price = fields.Float()
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False
)
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
property_type_id = fields.Many2one(related="property_id.property_type_id")
validity = fields.Integer(default=7, help="Validity in days")
date_deadline = fields.Date(
compute="_compute_offer_deadline", inverse="_inverse_deadline", readonly=False
)

@api.depends("validity")
def _compute_offer_deadline(self):
for ofr in self:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming convention should be good.

if isinstance(ofr.create_date, bool):
ofr.date_deadline = fields.Date.today() + relativedelta(
days=ofr.validity
)
else:
ofr.date_deadline = ofr.create_date + relativedelta(days=ofr.validity)

def _inverse_deadline(self):
for ofr in self:
ofr.validity = (ofr.date_deadline - ofr.create_date.date()).days

def accept_offer(self):
for offer in self:
other_accepted = offer.property_id.offer_ids.filtered(
lambda o: o.status == "accepted" and o != offer
)
if other_accepted:
raise UserError("Only one offer can be accepted.")

offer.status = "accepted"
offer.property_id.selling_price = offer.price
offer.property_id.buyer = offer.partner_id
offer.property_id.state = "offer_accepted"
return True

def refuse_offer(self):
self.status = "refused"
return True

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
state = self.env["estate.property"].browse(vals["property_id"]).state
if state == "sold":
raise UserError("Cannot create an offer for a sold property")
best_offer = (
self.env["estate.property"].browse(vals["property_id"]).best_price
)
if vals["price"] > best_offer:
self.env["estate.property"].browse(
vals["property_id"]
).state = "offer_received"
else:
errMes = f"The offer must be higher than {best_offer}"
raise UserError(errMes)
return super().create(vals_list)
13 changes: 13 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import fields, models


class PropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag"
_sql_constraints = [
("unique_property_tag_name", "UNIQUE(name)", "The name of tag must be unique."),
]
_order = "name"

name = fields.Char(required=True)
color = fields.Integer()
28 changes: 28 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from odoo import api, fields, models


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type"
_sql_constraints = [
(
"unique_property_type_name",
"UNIQUE(name)",
"The name of tag must be unique.",
),
]
_order = "name"

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id")
offer_ids = fields.One2many(related="property_ids.offer_ids")
offer_count = fields.Integer(compute="_compute_offer_count")
sequence = fields.Integer(
"Sequence", default=1, help="Used to order types. Lower is better."
)

@api.depends("offer_ids")
def _compute_offer_count(self):
for prp in self:
pricelist = prp.mapped("offer_ids")
prp.offer_count = len(pricelist)
11 changes: 11 additions & 0 deletions estate/models/inherited_estate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from odoo import fields, models


class InheritedModel(models.Model):
_inherit = "res.users"

property_ids = fields.One2many(
"estate.property",
"salesman",
domain=[("state", "in", ["new", "offer_received"])],
)
163 changes: 163 additions & 0 deletions estate/report/estate_property_offers_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_property_offers_document">
<t t-foreach="docs" t-as="property">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<t t-set="company" t-value="res_company" />
<strong>
<h2 t-esc="company.name" />
</strong>
<br />
<strong>Address:</strong>
<br />
<span t-esc="company.street" />
<br />
<t t-if="company.street2">
<span t-esc="company.street2" />
<br />
</t>
<span t-esc="company.city" />
<t t-if="company.state_id"> , <span t-esc="company.state_id.name" />
</t>
<t t-if="company.zip"> , <span t-esc="company.zip" />
</t>
<br />
<t t-if="company.country_id">
<span t-esc="company.country_id.name" />
</t>

<br />
<br />
<br />
<div class="page">
<strong>
<h2>
<span t-field="property.name" />
</h2>
</strong>
<div id="property_details">
<strong>Salesman: </strong>
<span t-field="property.salesman.name" />
<br />
<strong>Expected Price: </strong>
<span t-field="property.expected_price" />
<br />
<strong>Status: </strong>
<span t-field="property.state" />
</div>
<t t-set="offers" t-value="property.mapped('offer_ids')" />
<t t-if="len(offers) > 0">
<t t-call="estate.report_property_offers_table" />
</t>
<t t-else="">
<i>No offer has been made yet !:)</i>
</t>
</div>
</t>
</t>
</t>


</template>
<template id="report_salesman_property_document">
<t t-foreach="docs" t-as="salesman">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<t t-set="company" t-value="res_company" />
<strong>
<h2 t-esc="company.name" />
</strong>
<br />
<br />
<span t-esc="company.street" />
<br />
<t t-if="company.street2">
<span t-esc="company.street2" />
<br />
</t>
<span t-esc="company.city" />
<t t-if="company.state_id"> , <span t-esc="company.state_id.name" />
</t>
<t t-if="company.zip"> , <span t-esc="company.zip" />
</t>
<br />
<t t-if="company.country_id">
<span t-esc="company.country_id.name" />
</t>

<br />
<br />
<br />
<h1>
<strong>Salesman: </strong>
<span t-field="salesman.name" />
</h1>
<t t-foreach="salesman.property_ids" t-as="property">
<div class="page">
<br />
<br />
<strong>
<h2>
<span t-field="property.name" />
</h2>
</strong>
<div>

<strong>Expected Price: </strong>
<span t-field="property.expected_price" />
<br />
<strong>Status: </strong>
<span t-field="property.state" />
</div>
<t t-set="offers" t-value="property.mapped('offer_ids')" />
<t t-if="len(offers) > 0">
<t t-call="estate.report_property_offers_table" />
</t>
<t t-else="">
<i>No offer has been made yet !:)</i>
</t>
</div>
</t>
</t>
</t>
</t>


</template>

<template id="report_property_offers_table">
<table class="table" style="margin-top:8px;">
<thead>
<tr>
<th>Price</th>
<th>Partner</th>
<th>Validity(days)</th>
<th>Deadline</th>
<th>State</th>
</tr>
</thead>
<tbody>

<tr t-foreach="offers" t-as="offer">
<td>
<span t-field="offer.price" />
</td>
<td>
<span t-field="offer.partner_id.name" />
</td>
<td>
<span t-field="offer.validity" />
</td>
<td>
<span t-field="offer.date_deadline" />
</td>
<td>
<span t-field="offer.status" />
</td>
</tr>
</tbody>
</table>

</template>
</odoo>
22 changes: 22 additions & 0 deletions estate/report/estate_property_reports.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_report_property_offers" model="ir.actions.report">
<field name="name">Estate property offers/sale report</field>
<field name="model">estate.property</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">estate.report_property_offers_document</field>
<field name="report_file">estate.report_property_offers_document</field>
<field name="binding_model_id" ref="model_estate_property"/>
<field name="binding_type">report</field>
</record>

<record id="action_report_salesman_property" model="ir.actions.report">
<field name="name">Salesman's properties </field>
<field name="model">res.users</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">estate.report_salesman_property_document</field>
<field name="report_file">estate.report_salesman_property_document</field>
<field name="binding_model_id" ref="base.model_res_users"/>
<field name="binding_type">report</field>
</record>
</odoo>
24 changes: 24 additions & 0 deletions estate/security/estate_property_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="estate_property_rule_agent_own" model="ir.rule">
<field name="name">Agent: own or unassigned properties</field>
<field name="model_id" ref="estate.model_estate_property" />
<field name="domain_force">['|', ('salesman', '=', False), ('salesman', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('estate.estate_group_user'))]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="False" />
</record>

<record id="estate_property_rule_company" model="ir.rule">
<field name="name">Estate Agent: Company Access Restriction</field>
<field name="model_id" ref="estate.model_estate_property" />
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
<field name="groups" eval="[(4, ref('estate.estate_group_user'))]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="False" />
</record>
</odoo>
8 changes: 8 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_estate_property_manager,access_estate_property_manager,model_estate_property,estate.estate_group_manager,1,1,1,0
access_estate_property_user,access_estate_property_user,model_estate_property,estate.estate_group_user,1,1,1,0
access_property_type_user,access_estate_property_type_user,model_estate_property_type,estate.estate_group_user,1,0,0,0
access_property_tag_user,access_estate_property_tag_user,model_estate_property_tag,estate.estate_group_user,1,0,0,0
access_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.estate_group_manager,1,1,1,0
access_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate.estate_group_manager,1,1,1,0
access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
12 changes: 12 additions & 0 deletions estate/security/security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="estate_group_user" model="res.groups">
<field name="name">Agent</field>
<field name="category_id" ref="base.module_category_real_estate_brokerage" />
</record>

<record id="estate_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="base.module_category_real_estate_brokerage" />
</record>
</odoo>
1 change: 1 addition & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_property_offer
50 changes: 50 additions & 0 deletions estate/tests/test_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError


class TestPropertyOfferRules(TransactionCase):
@classmethod
def setUpClass(self):
super().setUpClass()
self.property = self.env["estate.property"].create(
{
"name": "Test Villa",
"expected_price": 200000,
"state": "new",
}
)
self.partner = self.env["res.partner"].create({"name": "Test Buyer"})

def test_cannot_create_offer_on_sold_property(self):
self.property.state = "sold"
with self.assertRaises(UserError):
self.env["estate.property.offer"].create(
{
"price": 180000,
"partner_id": self.partner.id,
"property_id": self.property.id,
}
)

def test_cannot_sell_without_accepted_offer(self):
self.env["estate.property.offer"].create(
{
"price": 180000,
"partner_id": self.partner.id,
"property_id": self.property.id,
}
)
with self.assertRaises(UserError):
self.property.set_sold_state()

def test_can_sell_with_accepted_offer(self):
self.env["estate.property.offer"].create(
{
"price": 190000,
"partner_id": self.partner.id,
"property_id": self.property.id,
"status": "accepted",
}
)
self.property.set_sold_state()
self.assertEqual(self.property.state, "sold")
13 changes: 13 additions & 0 deletions estate/views/estate_menus_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_first_level_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action" />
</menuitem>
<menuitem id="estate_first_level_menu_2" name="Settings"
groups="estate.estate_group_manager">
<menuitem id="estate_property_type_menu_action" action="estate_property_type_action" />
<menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action" />
</menuitem>
</menuitem>
</odoo>
48 changes: 48 additions & 0 deletions estate/views/estate_offers_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">estate.propertye.offer.form</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<form string="Offers">
<sheet>
<group>
<field name="price" />
<field name="partner_id" string="Partner" />
<field name="status" />
<field name="validity" />
<field name="date_deadline" />
</group>
</sheet>
</form>
</field>
</record>


<record id="estate_property_offer_view_tree" model="ir.ui.view">
<field name="name">estate.property.offer.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list string="Offers" editable="bottom" decoration-danger="status == 'refused'"
decoration-success="status == 'accepted'">
<field name="price" />
<field name="partner_id" string="Partner" />
<field name="property_type_id" string="Property Type" optional="hide" />
<button name="accept_offer" help="Accept" type="object" icon="fa-check"
invisible="status" />
<button name="refuse_offer" type="object" icon="fa-times" title="Refuse"
invisible="status" />
<field name="status" optional="hide" />
<field name="validity" />
<field name="date_deadline" />
</list>
</field>
</record>

<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list,form</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
</record>
</odoo>
54 changes: 54 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_type_view_form" model="ir.ui.view">
<field name="name">estate.propertye.type.form</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<form string="types">
<sheet>
<div class="oe_button_box" name="button_box">

<button class="oe_stat_button" name="%(estate_property_offer_action)d"
type="action" icon="fa-money">
<div class="o_stat_info">
<span class="o_stat_value">
<field name="offer_count" />
</span>
<span class="o_stat_text">
Offer
</span>
</div>
</button>
</div>
<h1 class="mb20">
<field name="name" class="mb16" />
</h1>
<notebook>
<page string="Properties">

<field name="property_ids">
<list>
<field name="name" />
<field name="expected_price" />
<field name="state" />
</list>
</field>

</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="estate_property_type_view_tree" model="ir.ui.view">
<field name="name">estate.property.type.list</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<list string="Properties">
<field name="name" />
<field name="sequence" widget="handle" />
</list>
</field>
</record>
</odoo>
135 changes: 135 additions & 0 deletions estate/views/estate_property_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_view_form" model="ir.ui.view">
<field name="name">estate.property.form</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<form string="Properties">
<header>
<button name="set_sold_state" type="object" class='btn-primary' string="Sold"
invisible="state == 'sold' or state == 'cancelled'" />
<button name="set_cancelled_state" type="object" string="Cancel"
invisible="state == 'sold' or state == 'cancelled'" />
<field name="state" widget="statusbar"
statusbar_visible="new,offer_received,offer_accepted,sold" />
</header>
<sheet>
<h1 class="mb20">
<field name="name" class="mb16" />
</h1>
<field name="tag_ids" editable="bottom" widget="many2many_tags" class="mb20"
options="{'color_field': 'color'}" />
<div class="mb20">
<group>
<group>
<field name="state" string="Status" />
<field name="property_type_id"
options="{'no_create': true, 'no_open': true}" />
<field name="postcode" />
<field name="expected_price" />
</group>
<group>
<field name="date_availability" />
<field name="best_price" />
<field name="selling_price" />
</group>
</group>
</div>
<notebook>
<page string="Description">
<group>
<field name="description" />
<field name="bedrooms" />
<field name="living_area" string="Living Area (sqm.)" />
<field name="facades" />
<field name="garage" />
<field name="garden" />
<field name="garden_orientation" invisible="not garden" />
<field name="garden_area" string="Garden Area (sqm.)"
invisible="not garden" />
<field name="total_area" />
</group>
</page>
<page string="Offers">
<group>
<field name="offer_ids"
readonly="state in ('sold', 'offer_accepted', 'cancelled')" />
</group>
</page>
<page string="Other info">
<group>
<field name="salesman" />
<field name="buyer" />
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="estate_property_search" model="ir.ui.view">
<field name="name">estate.property.search</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<search string="Properties">
<field name="name" />
<field name="tag_ids" widget="many2many_tags" />
<field name="property_type_id" />
<field name="postcode" />
<field name="expected_price" />
<field name="bedrooms" />
<field name="living_area" string="Living Area (sqm.)"
filter_domain="[('living_area', '&gt;=', self)]" />
<field name="facades" />
<filter string="Available" name="available_properties"
domain="['|', ('state', '=', 'new'), ('state','=','offer_received')]" />
<group expand="1" string="Group By">
<filter name="postcode" context="{'group_by':'postcode'}" />
<filter name="property_type_id" string="Property Type"
context="{'group_by':'property_type_id'}" />
</group>
</search>
</field>
</record>

<record id="estate_property_view_tree" model="ir.ui.view">
<field name="name">estate.property.list</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<list string="Properties" decoration-muted="state == 'sold'"
decoration-bf="state == 'offer_accepted'"
decoration-success="state == 'offer_received' or state == 'offer_accepted'">
<field name="name" />
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" />
<field name="property_type_id" />
<field name="postcode" />
<field name="bedrooms" />
<field name="living_area" string="Living Area (sqm.)" />
<field name="expected_price" />
<field name="selling_price" />
<field name="date_availability" string="Available from" optional="hide" />
</list>
</field>
</record>

<record id="estate_property_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="res_model">estate.property</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_available_properties': True,
'search_default_current': True}</field>
</record>

<record id="estate_property_type_action" model="ir.actions.act_window">
<field name="name">Property Type</field>
<field name="res_model">estate.property.type</field>
<field name="view_mode">list,form</field>
</record>

<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property Tag</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
15 changes: 15 additions & 0 deletions estate/views/res_users_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_users_view_form" model="ir.ui.view">
<field name="name">res.users.view.form.inherit.estate</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form" />
<field name="arch" type="xml">
<notebook position="inside">
<page string="Real Estate Properties">
<field name="property_ids" />
</page>
</notebook>
</field>
</record>
</odoo>
1 change: 1 addition & 0 deletions estate_account/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
7 changes: 7 additions & 0 deletions estate_account/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "Real Estate Account",
"depends": ["base", "estate", "account"],
"license": "LGPL-3",
"data": ["report/estate_property_inherited_template.xml"],
"application": True,
}
1 change: 1 addition & 0 deletions estate_account/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import estate_property
47 changes: 47 additions & 0 deletions estate_account/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from odoo import models


class InheritedProperty(models.Model):
_inherit = "estate.property"

def set_sold_state(self):
res = super().set_sold_state()

for property in self:
property.check_access("write")

if not property.buyer or not property.selling_price:
continue

commission = property.selling_price * 0.06
admin_fee = 100.0

self.env["account.move"].sudo().create(
{
"move_type": "out_invoice",
"partner_id": property.buyer.id,
"invoice_origin": property.name,
"invoice_line_ids": [
(
0,
0,
{
"name": "6% Commission",
"quantity": 1,
"price_unit": commission,
},
),
(
0,
0,
{
"name": "Administrative Fees",
"quantity": 1,
"price_unit": admin_fee,
},
),
],
}
)

return res
13 changes: 13 additions & 0 deletions estate_account/report/estate_property_inherited_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_property_offers_inherit_account"
inherit_id="estate.report_property_offers_document">
<xpath expr="//div[@id='property_details']" position="inside">
<t t-if="property.state == 'sold'">
<p>
<strong>Invoice has already been created!!!</strong>
</p>
</t>
</xpath>
</template>
</odoo>
1 change: 1 addition & 0 deletions odoo_self_order_details/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
16 changes: 16 additions & 0 deletions odoo_self_order_details/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "Odoo self order details",
"version": "1.0",
"depends": ["base", "pos_self_order"],
"license": "LGPL-3",
"assets": {
"pos_self_order.assets": [
"odoo_self_order_details/static/src/**/*",
],
},
"data": [
"views/product_template_view.xml",
],
"installable": True,
"application": True,
}
1 change: 1 addition & 0 deletions odoo_self_order_details/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import product_template
10 changes: 10 additions & 0 deletions odoo_self_order_details/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from odoo import models, fields


class ProductTemplate(models.Model):
_inherit = "product.template"

self_order_description = fields.Html(
string="Self Order Description",
translate=True,
)
18 changes: 18 additions & 0 deletions odoo_self_order_details/static/src/components/product_card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { patch } from "@web/core/utils/patch";
import { ProductCard } from "@pos_self_order/app/components/product_card/product_card";

patch(ProductCard.prototype, {
async selectProduct(qty = 1) {
const product = this.props.product;

if (!product.self_order_available || !this.isAvailable) {
return;
}

if (product.isCombo()) {
await super.selectProduct(qty);
} else {
this.router.navigate("product", { id: product.id });
}
},
});
25 changes: 25 additions & 0 deletions odoo_self_order_details/static/src/components/product_page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { patch } from "@web/core/utils/patch";
import { ProductPage } from "@pos_self_order/app/pages/product_page/product_page";
import { markup } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";

// Patch the ProductPage to include self_order_description
patch(ProductPage.prototype, {
async setup() {
super.setup();

const orm = useService("orm"); // or this.env.services.orm;

const [rc] = await orm.read(
"product.product",
[this.props.product.id],
["self_order_description"]
);

if (rc?.self_order_description) {
this.props.product.self_order_description = markup(
rc.self_order_description
);
}
},
});
36 changes: 36 additions & 0 deletions odoo_self_order_details/static/src/xml/product_page.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-inherit="pos_self_order.ProductPage" t-inherit-mode="extension">
<xpath expr="//h1[@class='mb-0 text-nowrap']" position="replace">
<h1 class="mb-0 text-nowrap"><strong t-esc="product.name"/> <span t-if="product.isConfigurable()">options</span></h1>
</xpath>

<xpath expr="//div[@class='pos_self_order_product_page_content d-flex flex-column flex-grow-1 overflow-y-auto']" position="replace">
<div class="p-3 d-flex flex-column flex-grow-1 overflow-y-auto">
<div class="d-flex justify-content-center">
<div style="max-width:auto;">
<img
class="o_self_order_item_card_image w-100 rounded"
t-attf-src="/web/image/product.product/{{ props.product.id }}/image_512"
alt="Product image"
loading="lazy"
onerror="this.remove()"
/>
</div>
</div>
<div class="p-3 d-flex justify-content-between align-items-center bg-200 rounded-top">
<h2 t-esc="product.name"/>
<h1 t-esc="selfOrder.formatMonetary(selfOrder.getProductDisplayPrice(product))"/>
</div>
<t t-if="product.self_order_description">
<div class="p-3 pt-0 bg-200 rounded-bottom text-600" t-out="product.self_order_description"/>
</t>
</div>
</xpath>
<xpath expr="//button[@class='btn btn-primary btn-lg']" position="before">
<button class="btn btn-secondary btn-lg px-3 text-nowrap" t-on-click="() => router.back()">
<span class="ms-2 d-md-inline">Cancel</span>
</button>
</xpath>
</t>
</templates>
15 changes: 15 additions & 0 deletions odoo_self_order_details/views/product_template_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_inherited_view" model="ir.ui.view">
<field name="name">product.template.form.inherit.template</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="arch" type="xml">
<data>
<xpath expr="//page[@name='pos']//group/group[@name='pos']" position="inside">
<field name="self_order_description" />
</xpath>
</data>
</field>
</record>
</odoo>
17 changes: 17 additions & 0 deletions sale_person/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Sale Person Attendance",
"version": "1.0",
"depends": ["base", "base_automation"],
"license": "LGPL-3",
"data": [
"views/sale_person_models.xml",
"views/contact_fields.xml",
"views/sale_person_fields.xml",
"views/contact_views.xml",
"views/sale_person_views.xml",
"views/sale_person_menus.xml",
"security/ir.model.access.csv",
],
"installable": True,
"application": True,
}
3 changes: 3 additions & 0 deletions sale_person/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_x_sale_person_sale_person_user,access_x_sale_person_sale_person_user,model_x_sale_person_sale_person,base.group_user,1,1,1,1
access_x_sale_person_contact_user,access_x_sale_person_contact_user,model_x_sale_person_contact,base.group_user,1,1,1,1
38 changes: 38 additions & 0 deletions sale_person/views/contact_fields.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="field_x_contact_name" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_contact" />
<field name="name">x_name</field>
<field name="ttype">char</field>
<field name="field_description">Name</field>
<field name="required">True</field>
</record>

<record id="field_x_contact_customer_type" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_contact" />
<field name="name">x_customer_type</field>
<field name="ttype">char</field>
<field name="field_description">Customer Type</field>
</record>

<record id="field_x_contact_city" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_contact" />
<field name="name">x_city</field>
<field name="ttype">char</field>
<field name="field_description">City</field>
</record>

<record id="field_x_contact_area" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_contact" />
<field name="name">x_area</field>
<field name="ttype">char</field>
<field name="field_description">Area</field>
</record>

<record id="field_x_contact_pincode" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_contact" />
<field name="name">x_pincode</field>
<field name="ttype">char</field>
<field name="field_description">Pincode</field>
</record>
</odoo>
45 changes: 45 additions & 0 deletions sale_person/views/contact_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<!-- Form & List view -->
<record id="view_x_sale_person_contact_list" model="ir.ui.view">
<field name="name">x.sale.person.contact.list</field>
<field name="model">x_sale_person_contact</field>
<field name="arch" type="xml">
<list string="Sale Person Contacts">
<field name="x_name" />
<field name="x_city" />
<field name="x_customer_type" />
</list>
</field>
</record>

<record id="view_x_sale_person_contact_form" model="ir.ui.view">
<field name="name">x.sale.person.contact.form</field>
<field name="model">x_sale_person_contact</field>
<field name="arch" type="xml">
<form string="Sales Person Contact">
<sheet>
<group>
<group>
<field name="x_name" />
<field name="x_customer_type" />
</group>
<group>
<field name="x_city" />
<field name="x_area" />
<field name="x_pincode" />
</group>
</group>
</sheet>
</form>
</field>
</record>

<!-- Action -->
<record id="contact_action" model="ir.actions.act_window">
<field name="name">Contact</field>
<field name="res_model">x_sale_person_contact</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
156 changes: 156 additions & 0 deletions sale_person/views/sale_person_fields.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="field_x_sale_person_id" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_sale_person_id</field>
<field name="ttype">many2one</field>
<field name="relation">res.users</field>
<field name="field_description">Sale Person</field>
</record>

<record id="field_x_checkin" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_check_in</field>
<field name="ttype">datetime</field>
<field name="required">1</field>
<field name="field_description">Check In</field>
</record>

<record id="field_x_checkout" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_check_out</field>
<field name="ttype">datetime</field>
<field name="field_description">Check Out</field>
</record>

<record id="field_x_customer" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_customer</field>
<field name="ttype">many2one</field>
<field name="relation">x_sale_person_contact</field>
<field name="field_description">Customer</field>
<field name="required">1</field>
<field name="on_delete">restrict</field>
</record>

<record id="field_x_customer_type" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_customer_type</field>
<field name="ttype">char</field>
<field name="field_description">Customer Type</field>
<field name="related">x_customer.x_customer_type</field>
</record>

<record id="field_x_city" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_city</field>
<field name="ttype">char</field>
<field name="field_description">City</field>
<field name="related">x_customer.x_city</field>
</record>

<record id="field_x_area" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_area</field>
<field name="ttype">char</field>
<field name="field_description">Area</field>
<field name="related">x_customer.x_area</field>
</record>
<record id="field_x_pincode" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_pincode</field>
<field name="ttype">char</field>
<field name="field_description">Pincode</field>
<field name="related">x_customer.x_pincode</field>
</record>
<record id="field_x_agenda" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_agenda</field>
<field name="ttype">char</field>
<field name="field_description">Agenda</field>
</record>
<record id="field_x_description" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_description</field>
<field name="ttype">char</field>
<field name="field_description">Description</field>
</record>

<record id="field_x_conversion_possibilities" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_conversion_possibilities</field>
<field name="ttype">selection</field>
<field name="selection" eval="[('high', 'High'), ('moderate', 'Moderate'), ('low', 'Low')]" />
<field name="field_description">Conversion Possibilities</field>
<field name="required">1</field>
</record>

<record id="field_x_worked_hours" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_worked_hours</field>
<field name="ttype">char</field>
<field name="field_description">Worked Hours</field>
</record>

<record id="field_x_checkin_location" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_checkin_location</field>
<field name="ttype">char</field>
<field name="field_description">CheckIn location</field>
</record>

<record id="field_x_checkout_location" model="ir.model.fields">
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="name">x_checkout_location</field>
<field name="ttype">char</field>
<field name="field_description">CheckOut location</field>
</record>


<!-- Automation & Server Actions -->

<record id="action_set_checkout" model="ir.actions.server">
<field name="name">Set Check-out Time</field>
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="binding_model_id" ref="model_x_sale_person_sale_person" />
<field name="state">code</field>
<field name="code">
checkout_time = datetime.datetime.now()
total_seconds = int((checkout_time - record.x_check_in).total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
worked_hours_str = f"{hours:02}:{minutes:02}"

record.write({
'x_check_out': checkout_time,
'x_worked_hours': worked_hours_str,
'x_checkout_location': 'Location Accessed [CheckOut]'
})
</field>
</record>

<record id="action_set_checkin" model="ir.actions.server">
<field name="name">Auto-fill Salesperson</field>
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="state">code</field>
<field name="code">
record.write({
'x_check_in': datetime.datetime.now(),
'x_worked_hours':'00:00',
'x_checkin_location':'Location Accessed [CheckIn]'
})
</field>
</record>

<!-- Automation to fill check_in data when form view opened. (Sale Person Id autofilled by
context so it will trigger this automation) -->
<record id="auto_action_fill_salesperson" model="base.automation">
<field name="name">Auto-fill Salesperson</field>
<field name="model_id" ref="model_x_sale_person_sale_person" />
<field name="trigger">on_change</field>
<field name="on_change_field_ids" eval="[(6,0,[ref('sale_person.field_x_sale_person_id')])]" />
<field name="action_server_ids" eval="[(4, ref('action_set_checkin'))]" />
<field name="active">1</field>
</record>
</odoo>
14 changes: 14 additions & 0 deletions sale_person/views/sale_person_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="sale_person_root" name="Sale Person">
<menuitem id="sale_person_first_level_menu"
name="Sale Person"
action="sale_person_action"
/>

<menuitem id="contact_first_level_menu"
name="Contacts"
action="contact_action"
/>
</menuitem>
</odoo>
17 changes: 17 additions & 0 deletions sale_person/views/sale_person_models.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Model name should start with 'x_' in xml defenatinos -->
<!-- Sale Person Model -->
<record id="model_x_sale_person_sale_person" model="ir.model">
<field name="name">Sale person</field>
<field name="model">x_sale_person_sale_person</field>
<field name="state">manual</field>
</record>

<!-- Contact Model -->
<record id="model_x_sale_person_contact" model="ir.model">
<field name="name">Sale person contact</field>
<field name="model">x_sale_person_contact</field>
<field name="state">manual</field>
</record>
</odoo>
68 changes: 68 additions & 0 deletions sale_person/views/sale_person_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form and List view -->
<record id="view_x_sale_person_list" model="ir.ui.view">
<field name="name">x.sale.person.list</field>
<field name="model">x_sale_person_sale_person</field>
<field name="arch" type="xml">
<list string="Sale Person">
<field name="x_sale_person_id" />
<field name="x_check_in" />
<field name="x_check_out" />
<field name="x_customer" />
<field name="x_conversion_possibilities" />
<field name="x_worked_hours" />
</list>
</field>
</record>

<record id="view_x_sale_person_form" model="ir.ui.view">
<field name="name">x.sale.person.form</field>
<field name="model">x_sale_person_sale_person</field>
<field name="arch" type="xml">
<form string="Sales Person">
<header>
<button name="%(action_set_checkout)d"
string="Check Out"
type="action"
class="btn-primary"
invisible="x_check_out != False"
/>
</header>
<sheet>
<group>
<group>
<!-- Readonly fields not stored in DB by odoo so to make it stored add attr 'force_save="1"' -->
<field name="x_sale_person_id" readonly="1" />
<field name="x_check_in" readonly="1" force_save="1" />
<field name="x_check_out" readonly="1" force_save="1" />
<field name="x_customer" options="{'no_create': true, 'no_open': true}" />
<field name="x_customer_type" readonly="1" />
</group>
<group>
<field name="x_city" readonly="1" />
<field name="x_area" readonly="1" />
<field name="x_pincode" readonly="1" />
<field name="x_agenda" />
<field name="x_description" />
<field name="x_conversion_possibilities" />
<field name="x_worked_hours" readonly="1" force_save="1" />
</group>
</group>
<group>
<field name="x_checkin_location" groups="base.group_system" readonly="1" force_save="1" />
<field name="x_checkout_location" groups="base.group_system" readonly="1" force_save="1" />
</group>
</sheet>
</form>
</field>
</record>

<!-- Action -->
<record id="sale_person_action" model="ir.actions.act_window">
<field name="name">Sale person</field>
<field name="res_model">x_sale_person_sale_person</field>
<field name="view_mode">list,form</field>
<field name="context">{'default_x_sale_person_id': uid}</field> <!-- set default value of 'x_sale_person_id' to 'uid' (current logged in user) -->
</record>
</odoo>
2 changes: 2 additions & 0 deletions sell_products_kit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
15 changes: 15 additions & 0 deletions sell_products_kit/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Sell products kit",
"version": "1.0",
"depends": ["sale_management"],
"license": "LGPL-3",
"data": [
"views/product_template_view.xml",
"views/sales_order_view.xml",
"security/ir.model.access.csv",
"wizard/sub_product_wizard_view.xml",
"wizard/sub_product_wizard_line_view.xml",
],
"installable": True,
"application": True,
}
3 changes: 3 additions & 0 deletions sell_products_kit/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import product_template
from . import sale_order_line
from . import sale_order
8 changes: 8 additions & 0 deletions sell_products_kit/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import models, fields


class ProductTemplate(models.Model):
_inherit = "product.template"

is_kit = fields.Boolean()
sub_products_ids = fields.Many2many("product.product", string="Sub products")
35 changes: 35 additions & 0 deletions sell_products_kit/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from odoo import api, fields, models


class SaleOrder(models.Model):
_inherit = "sale.order"

print_in_report = fields.Boolean(
default=False, help="Print kits sub products in the report"
)

@api.onchange("order_line")
def _onchange_order(self):
current_kit_ids = {line._origin.id for line in self.order_line if line.product_is_kit}

# filter out lines that are not kits and whose parent kit is not in the current kit ids (parent is_kit is deleted)
new_order_lines = self.order_line.filtered(
lambda line: not line.parent_kit_id.id
or (line.parent_kit_id.id in current_kit_ids)
)

self.order_line = new_order_lines

def _get_order_lines_to_report(self):
order_lines = super()._get_order_lines_to_report()
if self.print_in_report:
return order_lines
else:
return order_lines.filtered(lambda line: line.product_is_kit)

def _get_invoiceable_lines(self, final=False):
invoicable_lines = super()._get_invoiceable_lines(final=final)
if self.print_in_report:
return invoicable_lines
else:
return invoicable_lines.filtered(lambda line: line.product_is_kit)
28 changes: 28 additions & 0 deletions sell_products_kit/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from odoo import api, fields, models


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

product_is_kit = fields.Boolean(
compute="_compute_product_is_kit",
)
parent_kit_id = fields.Many2one("sale.order.line")
original_price_unit = fields.Float()

@api.depends("product_id")
def _compute_product_is_kit(self):
for line in self:
line.product_is_kit = line.product_id.product_tmpl_id.is_kit
if not line.original_price_unit:
line.original_price_unit = line.price_unit

def open_sub_prod(self):
return {
"name": f"Product : {self.product_id.display_name}",
"type": "ir.actions.act_window",
"res_model": "sale.sub.product.wizard",
"view_mode": "form",
"target": "new",
"context": {"active_id": self.id},
}
3 changes: 3 additions & 0 deletions sell_products_kit/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
access_sub_product_wizard_all,access.sub.product.wizard.all,model_sale_sub_product_wizard,,1,1,1,1
access_sub_product_wizard_line_all,access.sub.product.wizard.line.all,model_sale_sub_product_wizard_line,,1,1,1,1
18 changes: 18 additions & 0 deletions sell_products_kit/views/product_template_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_form_inherited_view" model="ir.ui.view">
<field name="name">product.template.form.inherit.template</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="arch" type="xml">
<data>
<xpath
expr="//page[@name='general_information']//group/group[@name='group_general']"
position="inside">
<field name="is_kit" />
<field name="sub_products_ids" invisible="not is_kit" widget="many2many_tags" />
</xpath>
</data>
</field>
</record>
</odoo>
43 changes: 43 additions & 0 deletions sell_products_kit/views/sales_order_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_form_view_inherited" model="ir.ui.view">
<field name="name">sale.order.form.inherited</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<xpath expr="//group[@name='order_details']" position="inside">
<field name="print_in_report" />
</xpath>

<xpath expr="//field[@name='product_template_id']" position="after">
<button type="object" name="open_sub_prod" string="Sub Products"
invisible="not product_is_kit or state == 'sale'" />
</xpath>

<xpath expr="//list" position="attributes">
<attribute name="decoration-warning" add="parent_kit_id" separator=" or " />
</xpath>

<!-- Change readonly attribute of order_line for sub_products -->

<xpath expr="//field[@name='product_template_id']" position="attributes">
<attribute name="readonly" add="parent_kit_id" separator=" or " />
</xpath>
<xpath expr="(//field[@name='product_id'])[2]" position="attributes">
<attribute name="readonly" add="parent_kit_id" separator=" or " />
</xpath>
<xpath expr="(//field[@name='product_uom_qty'])[2]" position="attributes">
<attribute name="readonly" add="parent_kit_id" separator=" or " />
</xpath>
<xpath expr="(//field[@name='customer_lead'])[2]" position="attributes">
<attribute name="readonly" add="parent_kit_id" separator=" or " />
</xpath>
<xpath expr="(//field[@name='price_unit'])[2]" position="attributes">
<attribute name="readonly" add="parent_kit_id" separator=" or " />
</xpath>
<xpath expr="(//field[@name='tax_id'])[2]" position="attributes">
<attribute name="readonly" add="parent_kit_id" separator=" or " />
</xpath>
</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions sell_products_kit/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import sub_product_wizard
from . import sub_product_wizard_line
106 changes: 106 additions & 0 deletions sell_products_kit/wizard/sub_product_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from odoo import models, fields, api


class SubProductWizard(models.TransientModel):
_name = "sale.sub.product.wizard"
_description = "Select Sub Products"

sale_order_id = fields.Many2one("sale.order", string="Sale Order", readonly=True)

sub_product_wizard_line_ids = fields.One2many(
"sale.sub.product.wizard.line", "sub_product_wizard_id"
)
sub_product_ids = fields.Many2many(
"product.product", string="Sub Products", readonly=True
)
main_line_id = fields.Many2one(
"sale.order.line", string="Main Kit Line", readonly=True
)
cost = fields.Float(string="Total Cost", compute="_compute_total_cost")

@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
active_id = self.env.context.get("active_id")

line = self.env["sale.order.line"].browse(active_id)

sub_products = line.product_id.product_tmpl_id.sub_products_ids

sub_products_lines = []

prev_sub_prod = line.order_id.order_line.filtered(
lambda ln: ln.parent_kit_id.id == line.id
)

def add_sub_product_line(data):
sub_products_lines.append((0, 0, data))

if prev_sub_prod and len(prev_sub_prod) == len(sub_products):
for product, prev_prod in zip(sub_products, prev_sub_prod):
add_sub_product_line(
{
"product_id": product.id,
"price": product.list_price,
"quantity": prev_prod.product_uom_qty,
}
)
else:
for product in sub_products:
add_sub_product_line(
{
"product_id": product.id,
"price": product.list_price,
}
)

res.update(
{
"main_line_id": line.id,
"sale_order_id": line.order_id.id,
"sub_product_wizard_line_ids": sub_products_lines,
"sub_product_ids": [(6, 0, sub_products.ids)],
}
)
return res

def action_add_sub_products(self):
for line, sub_product in zip(
self.sub_product_wizard_line_ids, self.sub_product_ids
):
exisating_lines = self.sale_order_id.order_line.filtered(
lambda ln: ln.product_id.id == sub_product.id
and ln.parent_kit_id.id == self.main_line_id.id
)

if exisating_lines:
exisating_lines.write(
{
"product_uom_qty": line.quantity,
"price_unit": 0.0,
}
)
else:
self.env["sale.order.line"].create(
{
"order_id": self.sale_order_id.id,
"product_id": sub_product.id,
"product_uom_qty": line.quantity,
"price_unit": 0.0,
"parent_kit_id": self.main_line_id.id,
}
)

main_prod_cost = self.main_line_id.original_price_unit + self.cost
self.main_line_id.write({"price_unit": main_prod_cost})

return {"type": "ir.actions.act_window_close"}

@api.depends("sub_product_wizard_line_ids.quantity")
def _compute_total_cost(self):
for wizard in self:
total_cost = sum(
(line.price * line.quantity)
for line in wizard.sub_product_wizard_line_ids
)
wizard.cost = total_cost
10 changes: 10 additions & 0 deletions sell_products_kit/wizard/sub_product_wizard_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from odoo import models, fields


class SubProductWizardLine(models.TransientModel):
_name = "sale.sub.product.wizard.line"

product_id = fields.Many2one("product.product", string="Product")
quantity = fields.Float(default=1.0, required=True)
price = fields.Float(default=0.0, required=True)
sub_product_wizard_id = fields.Many2one(comodel_name="sale.sub.product.wizard")
18 changes: 18 additions & 0 deletions sell_products_kit/wizard/sub_product_wizard_line_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sub_product_wizard_line" model="ir.ui.view">
<field name="name">sub.product.wizard.line.list</field>
<field name="model">sale.sub.product.wizard.line</field>
<field name="arch" type="xml">
<list
string="Sub products Lines"
editable="bottom"
create="False"
>
<field name="product_id" readonly="1" required="1" />
<field name="quantity" />
<field name="price" />
</list>
</field>
</record>
</odoo>
29 changes: 29 additions & 0 deletions sell_products_kit/wizard/sub_product_wizard_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sub_product_wizard_form" model="ir.ui.view">
<field name="name">sub.product.wizard.form</field>
<field name="model">sale.sub.product.wizard</field>
<field name="arch" type="xml">
<form>
<h3>Sub Products</h3>
<field name="sub_product_wizard_line_ids" />
<group>
<field name="cost" readonly="1" />
</group>
<footer>
<button string="Confirm" type="object" name="action_add_sub_products"
class="btn-primary" />
<button string="Cancel" special="cancel" class="btn-secondary" />
</footer>
</form>
</field>
</record>

<record id="action_sub_product_wizard" model="ir.actions.act_window">
<field name="name">Sub products</field>
<field name="res_model">sale.sub.product.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>

</odoo>