-
Notifications
You must be signed in to change notification settings - Fork 2.3k
[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
base: 18.0
Are you sure you want to change the base?
Changes from all commits
210de3a
2372781
edb6418
ea044c5
334eb7c
ee3661f
ae6bd0a
2b8c3fb
2f6fde4
7e9a1a4
26bf362
7c237dc
e0eecd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
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); |
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; | ||
} | ||
} |
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> |
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, | ||
}; | ||
} |
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> |
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(); | ||
} | ||
} |
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> |
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, | ||
}, | ||
}, | ||
}, | ||
}); | ||
} | ||
} |
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> |
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); |
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); |
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; | ||
} | ||
} |
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> |
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(); | ||
} | ||
} |
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> |
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++; | ||
} | ||
} |
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'] | ||
|
||
} |
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> |
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; | ||
} | ||
} | ||
} |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
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, | ||
} |
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> |
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> |
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> |
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 |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.") |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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() |
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) |
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"])], | ||
) |
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> |
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> |
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> |
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 |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_property_offer |
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") |
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> |
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> |
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> |
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', '>=', 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> |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
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, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import estate_property |
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 |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
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, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import product_template |
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, | ||
) |
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 }); | ||
} | ||
}, | ||
}); |
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 | ||
); | ||
} | ||
}, | ||
}); |
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> |
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> |
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, | ||
} |
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 |
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> |
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> |
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> |
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> |
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> |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import models | ||
from . import wizard |
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, | ||
} |
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 |
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") |
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) |
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}, | ||
} |
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 |
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> |
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> |
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 |
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 |
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") |
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> |
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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary changes.