- hello world
+
+
Owl Playground
+
+
+
+
Counters
+
Sum:
+
+
+
+
+
+
+
+
+
+
+
+ This is another card. Add your content here!
+
+
+
+
-
diff --git a/awesome_owl/static/src/todo_list/todo_items.js b/awesome_owl/static/src/todo_list/todo_items.js
new file mode 100644
index 00000000000..85630f8c450
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_items.js
@@ -0,0 +1,30 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todo_item";
+ static props = {
+ todo: {
+ type: Object,
+ shape: {
+ id: { type: Number, optional: false },
+ description: { type: String, optional: false },
+ isCompleted: { type: Boolean, optional: false },
+ },
+ optional: false,
+ },
+ toggleState: { type: Function, optional: false },
+ removeTodo: { type: Function, optional: false }, // New callback prop for deletion
+ };
+
+ toggleState() {
+ // Call the parent's toggleState function with the todo id
+ this.props.toggleState(this.props.todo.id);
+ }
+
+ removeTodo() {
+ // Call the parent's removeTodo function with the todo id
+ this.props.removeTodo(this.props.todo.id);
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_items.xml b/awesome_owl/static/src/todo_list/todo_items.xml
new file mode 100644
index 00000000000..bfc953e9c3d
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_items.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ .
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js
new file mode 100644
index 00000000000..ccefb543ba1
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.js
@@ -0,0 +1,43 @@
+import { Component, useState } from "@odoo/owl";
+import { TodoItem } from "./todo_items";
+import { useAutoFocus } from "../utils";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.todo_list";
+ static components = { TodoItem };
+ setup() {
+ useAutoFocus("todoInput");
+ this.todos = useState([]);
+ this.nextId = 1;
+ }
+
+ addTodo(event) {
+ if (event.keyCode === 13) {
+ const description = event.target.value.trim();
+ if (description) {
+ this.todos.push({
+ id: this.nextId++,
+ description: description,
+ isCompleted: false,
+ });
+ event.target.value = "";
+ }
+ }
+ }
+
+ toggleTodo(todoId) {
+ const todo = this.todos.find(t => t.id === todoId);
+ if (todo) {
+ todo.isCompleted = !todo.isCompleted;
+ }
+ }
+
+ removeTodo(todoId) {
+ // Find the index of the todo to remove
+ const index = this.todos.findIndex(t => t.id === todoId);
+ if (index !== -1) {
+ // Remove the todo from the array
+ this.todos.splice(index, 1);
+ }
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml
new file mode 100644
index 00000000000..69ae97918f8
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
Todo List
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..02a5eba74db
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,13 @@
+/** @odoo-module **/
+
+import { onMounted, useRef } from "@odoo/owl";
+
+export function useAutoFocus(refname) {
+ const inputRef = useRef(refname)
+
+ onMounted(() => {
+ if (inputRef.el) {
+ inputRef.el.focus();
+ }
+ });
+}
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..2e8515d484d
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,30 @@
+{
+ "name": "estate",
+ "description": """
+ This module is used to manage the Real estate and properties.
+ """,
+ "author": "ayush",
+ "version": "0.1",
+ "application": True,
+ "installable": True,
+ "depends": ["base"],
+ "license": "LGPL-3",
+ "category": "Real Estate/Brokerage",
+ "data": [
+ "security/estate_security.xml",
+ "security/ir.model.access.csv",
+ "views/estate_property_views.xml",
+ "views/estate_property_type_views.xml",
+ "views/estate_property_tags_views.xml",
+ "views/estate_property_offer_views.xml",
+ "views/estate_menus.xml",
+ "views/res_users_views.xml",
+ "data/estate_property_type_demo.xml",
+ "reports/estate_property_templates.xml",
+ "reports/estate_property_reports.xml",
+ ],
+ "demo": [
+ "data/estate_property_demo.xml",
+ "data/estate_property_offer_demo.xml",
+ ],
+}
diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml
new file mode 100644
index 00000000000..eae66d14300
--- /dev/null
+++ b/estate/data/estate_property_demo.xml
@@ -0,0 +1,65 @@
+
+
+
+ Big Villa
+ new
+ A nice and big Villa
+ 30050
+
+ 1600000
+ 6
+ 500
+ 4
+ True
+ True
+ 1200
+ south
+
+
+
+ Ocean side Mansion
+ cancelled
+ Grand ocean side mansion with stunning views of ocean
+ 50052
+
+ 1200000
+ 0
+ 4
+ 1000
+ 4
+ True
+
+
+
+ Empire tower's Luxurious Penthouse
+ offer_received
+ A luxurious penthouse with views of central park
+ 65065
+
+ 1800000
+ 6
+ 5000
+ 2
+ False
+ False
+
+
+
+
+
diff --git a/estate/data/estate_property_offer_demo.xml b/estate/data/estate_property_offer_demo.xml
new file mode 100644
index 00000000000..46b06f9756a
--- /dev/null
+++ b/estate/data/estate_property_offer_demo.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 1440000
+ 14
+
+
+
+
+
+ 1500000
+ 14
+
+
+
+
+
+ 1550000
+ 14
+
+
+
+
+
+
+
diff --git a/estate/data/estate_property_type_demo.xml b/estate/data/estate_property_type_demo.xml
new file mode 100644
index 00000000000..47f34ede85a
--- /dev/null
+++ b/estate/data/estate_property_type_demo.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Residential
+
+
+ Commercial
+
+
+ Land
+
+
+ Industrial
+
+
+
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..6315ba30deb
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import estate_property
+from . import estate_property_type
+from . import estate_property_tags
+from . import estate_property_offer
+from . import res_users
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..9279304fa42
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,169 @@
+from odoo import models, fields, api
+from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError, ValidationError
+
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "Estate Property"
+ _sql_constraints = [
+ (
+ "estate_property_expected_price_positive",
+ "CHECK(expected_price > 0)",
+ "The expected price must be positive.",
+ ),
+ (
+ "estate_property_selling_price_non_negative",
+ "CHECK(selling_price >= 0)",
+ "The selling price must be non negative.",
+ ),
+ ]
+
+ _order = "id desc"
+
+ name = fields.Char(string="Property Name", required=True)
+ description = fields.Text(string="Description")
+ postcode = fields.Char(string="Postcode")
+ date_availability = fields.Date(
+ string="Available From",
+ copy=False,
+ default=fields.Date.today() + relativedelta(months=4),
+ )
+ expected_price = fields.Float(string="Expected Price")
+ selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
+ bedrooms = fields.Integer(string="Bedrooms", default=2)
+ living_area = fields.Integer(string="Living Area (sqm)")
+ facades = fields.Integer(string="Facades")
+ garage = fields.Boolean(string="Garage")
+ garden = fields.Boolean(string="Garden")
+ garden_area = fields.Integer(string="Garden Area (sqm)")
+ garden_orientation = fields.Selection(
+ selection=[
+ ("north", "North"),
+ ("south", "South"),
+ ("east", "East"),
+ ("west", "West"),
+ ],
+ string="Garden Orientation",
+ )
+ active = fields.Boolean(string="Active", default=True)
+ state = fields.Selection(
+ selection=[
+ ("new", "New"),
+ ("offer_received", "Offer Received"),
+ ("offer_accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("cancelled", "Cancelled"),
+ ],
+ string="Status",
+ required=True,
+ copy=False,
+ default="new",
+ )
+ property_type_id = fields.Many2one("estate.property.type", string="Property Type")
+ buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
+ salesperson_id = fields.Many2one(
+ "res.users", string="Salesperson", default=lambda self: self.env.user
+ )
+ tag_ids = fields.Many2many(
+ "estate.property.tags", string="Tags", help="Tags for the property"
+ )
+ offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+ total_area = fields.Integer(
+ string="Total Area (sqm)",
+ compute="_compute_total_area",
+ store=True,
+ )
+ company_id = fields.Many2one(
+ "res.company", required=True, default=lambda self: self.env.company
+ )
+ best_price = fields.Float(
+ string="Best Price",
+ compute="_compute_best_price",
+ store=True,
+ )
+
+ @api.depends("living_area", "garden_area")
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.living_area + record.garden_area
+
+ @api.depends("offer_ids.price")
+ def _compute_best_price(self):
+ for record in self:
+ if record.offer_ids:
+ record.best_price = max(record.offer_ids.mapped("price"))
+ else:
+ record.best_price = 0.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 action_set_sold(self):
+ for offer in self:
+ if offer.state == "sold":
+ raise UserError("This property is already sold.")
+ if offer.state == "offer_accepted":
+ offer.state = "sold"
+ return True
+ else:
+ raise UserError("Only accepted offers can set the property as sold.")
+
+ def action_set_cancel(self):
+ for offer in self:
+ if offer.state == "new":
+ offer.state = "cancelled"
+ return True
+ else:
+ raise UserError(
+ "Only refused offers can set the property as cancelled."
+ )
+
+ @api.constrains("selling_price", "expected_price")
+ def _check_price(self):
+ for record in self:
+ if record.selling_price and record.expected_price:
+ if record.selling_price < 0.9 * record.expected_price:
+ raise ValidationError(
+ "The selling price must be at least 90% of the expected price."
+ )
+
+ @api.ondelete(at_uninstall=False)
+ def _ondelete_property(self):
+ for record in self:
+ if record.state not in ["new", "cancelled"]:
+ raise UserError(
+ "You cannot delete a property that is not new or cancelled."
+ )
+
+ @api.onchange("offer_ids")
+ def _onchange_offer_ids(self):
+ if not self.offer_ids and self.state != "new":
+ self.state = "new"
+
+ @api.onchange("state")
+ def _onchange_state(self):
+ if self.state == "offer_received" and not self.offer_ids:
+ raise UserError("No offers available yet!.")
+ elif self.state == "offer_received" and self.offer_ids.filtered(
+ lambda o: o.status == "accepted"
+ ):
+ raise UserError(
+ "You cannot set the property as offer received when there is an accepted offer."
+ )
+ elif self.state == "offer_accepted" and not self.offer_ids:
+ raise UserError("You cannot accept an offer without any offers.")
+ elif self.state == "sold" and not self.offer_ids:
+ raise UserError("You cannot sell a property without any offers.")
+ elif self.state == "offer_accepted" and not self.offer_ids.filtered(
+ lambda o: o.status == "accepted"
+ ):
+ raise UserError(
+ "You cannot set the property as offer accepted without an accepted offer."
+ )
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..626cdf4462e
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,133 @@
+from odoo import models, fields, api
+from dateutil.relativedelta import relativedelta
+from odoo.exceptions import UserError, ValidationError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Estate Property Offer"
+ _sql_constraints = [
+ (
+ "estate_property_offer_price_positive",
+ "CHECK(price > 0)",
+ "The offer price must be strictly positive.",
+ )
+ ]
+
+ _order = "price desc"
+
+ price = fields.Float(string="Price", required=True)
+ status = fields.Selection(
+ [
+ ("accepted", "Accepted"),
+ ("refused", "Refused"),
+ ],
+ string="Status",
+ copy=False,
+ )
+ partner_id = fields.Many2one("res.partner", string="Partner", required=True)
+ property_id = fields.Many2one("estate.property", string="Property", required=True)
+ validity = fields.Integer(string="Validity (days)", default=7)
+ date_deadline = fields.Date(
+ string="Deadline",
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ store=True,
+ )
+ create_date = fields.Datetime(
+ string="Creation Date", readonly=True, default=fields.Datetime.now
+ )
+ property_type_id = fields.Many2one(
+ "estate.property.type",
+ string="Property Type",
+ related="property_id.property_type_id",
+ store=True,
+ )
+
+ @api.depends("create_date", "validity")
+ def _compute_date_deadline(self):
+ for offer in self:
+ creation_date = (
+ offer.create_date.date() if offer.create_date else fields.Date.today()
+ )
+ if offer.validity:
+ offer.date_deadline = creation_date + relativedelta(days=offer.validity)
+ else:
+ offer.date_deadline = creation_date
+
+ def _inverse_date_deadline(self):
+ for offer in self:
+ creation_date = (
+ offer.create_date.date() if offer.create_date else fields.Date.today()
+ )
+ if offer.date_deadline:
+ offer.validity = (offer.date_deadline - creation_date).days
+ else:
+ offer.validity = 0
+
+ def action_accept_offer(self):
+ for record in self:
+ record_property = record.property_id
+ property_state = record_property.state
+
+ if property_state == "offer_accepted":
+ raise UserError("You can only accept one offer at a time.")
+ if property_state == "sold":
+ raise UserError("You cannot accept an offer on a sold property.")
+ if property_state == "cancelled":
+ raise UserError("You cannot accept an offer on a cancelled property.")
+
+ other_offers = record_property.offer_ids.filtered(
+ lambda o: o.id != record.id
+ )
+ other_offers.write({"status": "refused"})
+
+ record.status = "accepted"
+ record_property.write(
+ {
+ "buyer_id": record.partner_id.id,
+ "selling_price": record.price,
+ "state": "offer_accepted",
+ }
+ )
+
+ return True
+
+ def action_refuse_offer(self):
+ for record in self:
+ record_property = record.property_id
+ property_state = record_property.state
+
+ if property_state in ["sold", "cancelled"]:
+ raise UserError(
+ "You cannot refuse an offer on a sold or cancelled property."
+ )
+ if record.status == "accepted":
+ raise UserError("You cannot refuse an already accepted offer.")
+
+ record.status = "refused"
+ return True
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ estate_property_model_instance = self.env["estate.property"]
+ for vals in vals_list:
+ property_id = vals.get("property_id")
+ estate_property = estate_property_model_instance.browse(property_id)
+ best_price = estate_property.best_price
+ if not estate_property:
+ raise ValidationError("Property not found.")
+
+ if estate_property.state in ["sold", "cancelled"]:
+ raise ValidationError(
+ "You cannot create an offer for a sold or cancelled property."
+ )
+
+ if best_price >= vals.get("price", 0.0):
+ raise ValidationError(
+ "The offer price must be strictly higher than the previous offers."
+ )
+ best_price = max(best_price, vals.get("price", 0.0))
+ estate_property.state = "offer_received"
+
+ return super().create(vals_list)
diff --git a/estate/models/estate_property_tags.py b/estate/models/estate_property_tags.py
new file mode 100644
index 00000000000..211c2f18d1b
--- /dev/null
+++ b/estate/models/estate_property_tags.py
@@ -0,0 +1,18 @@
+from odoo import models, fields
+
+
+class EstatePropertyTags(models.Model):
+ _name = "estate.property.tags"
+ _description = "Estate Property Tags"
+ _sql_constraints = [
+ (
+ "estate_property_tag_name_unique",
+ "UNIQUE(name)",
+ "The tag names must be unique.",
+ )
+ ]
+
+ _order = "name"
+
+ name = fields.Char(string="Tag Name", required=True)
+ color = fields.Integer(string="Color Index")
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..f8b77b9a7c0
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,28 @@
+from odoo import fields, models, api
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Real Estate Property Type"
+ _sql_constraints = [
+ (
+ "estate_property_type_name_unique",
+ "UNIQUE(name)",
+ "The type names must be unique.",
+ )
+ ]
+
+ _order = "sequence, name"
+
+ name = fields.Char(required=True)
+ sequence = fields.Integer("Sequence")
+ property_ids = fields.One2many("estate.property", "property_type_id")
+ offer_ids = fields.One2many("estate.property.offer", "property_type_id")
+ offer_count = fields.Integer(
+ compute="_compute_offer_count", string="Offer Count", readonly=True, copy=False
+ )
+
+ @api.depends("offer_ids")
+ def _compute_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.offer_ids)
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..a051f025c06
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,12 @@
+from odoo import models, fields
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ "estate.property",
+ "salesperson_id",
+ string="Properties",
+ domain=[("state", "in", ["new", "offer_received"])],
+ )
diff --git a/estate/reports/estate_property_reports.xml b/estate/reports/estate_property_reports.xml
new file mode 100644
index 00000000000..3312c911e42
--- /dev/null
+++ b/estate/reports/estate_property_reports.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ Estate Property Sale Report
+ estate.property
+ qweb-pdf
+ estate.report_property_offers
+ estate.report_property_offers
+ 'Property Offers - ' + object.name
+
+ report
+
+
+
+
+ Salesperson Properties
+ res.users
+ estate.report_salesperson_property
+ estate.report_salesperson_property
+ 'Salesperson Properties - ' + object.name
+
+ report
+
+
diff --git a/estate/reports/estate_property_templates.xml b/estate/reports/estate_property_templates.xml
new file mode 100644
index 00000000000..acbddef08f2
--- /dev/null
+++ b/estate/reports/estate_property_templates.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+ Offer price |
+ Partner |
+ Validity (days) |
+ Deadline |
+ State |
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+ Pending
+ |
+
+
+
+
+ No offers available for this property.
+
+
+
+
+
+
+
+
+
+
+
+ Salesperson:
+
+
+ Expected Price:
+
+
+ Status:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Salesperson:
+
+
+
+
+
+
+ Expected Price:
+
+
+ Status:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml
new file mode 100644
index 00000000000..43a06965654
--- /dev/null
+++ b/estate/security/estate_security.xml
@@ -0,0 +1,44 @@
+
+
+
+ Agent
+
+
+
+
+
+ Estate Manager
+
+
+
+
+
+
+
+ Estate Property Agent Access
+
+
+
+
+ ['|', ('salesperson_id', '=', user.id), ('salesperson_id', '=', False)]
+
+
+
+
+ Estate Property Manager All Access
+
+
+
+
+
+
+
+ Agents can see only their company's data
+
+ [
+ '|', ('company_id', '=', False),
+ ('company_id', 'in', company_ids)
+ ]
+
+
+
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..eaa33aaaf2f
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,9 @@
+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_group_manager,1,1,1,1
+access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate_group_manager,1,1,1,1
+access_estate_property_tags_manager,access_estate_property_tags_manager,model_estate_property_tags,estate_group_manager,1,1,1,1
+access_estate_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate_group_manager,1,1,1,1
+access_estate_property_agent,access_estate_property_agent,model_estate_property,estate_group_user,1,1,1,0
+access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,0,0,0
+access_estate_property_tags_agent,access_estate_property_tags_agent,model_estate_property_tags,estate_group_user,1,0,0,0
+access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0
diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py
new file mode 100644
index 00000000000..524f7bf1293
--- /dev/null
+++ b/estate/tests/__init__.py
@@ -0,0 +1 @@
+from . import estate_property_tests
diff --git a/estate/tests/estate_property_tests.py b/estate/tests/estate_property_tests.py
new file mode 100644
index 00000000000..b692795af5b
--- /dev/null
+++ b/estate/tests/estate_property_tests.py
@@ -0,0 +1,79 @@
+from odoo import Command # noqa: F401
+from odoo.exceptions import UserError
+from odoo.tests import tagged
+from odoo.tests import Form, TransactionCase
+
+
+@tagged("post_install", "-at_install")
+class EstateTestCase(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.properties = cls.env["estate.property"].create(
+ [
+ {
+ "name": "Test Property",
+ "description": "Some Description",
+ "expected_price": 100000,
+ "living_area": 50,
+ },
+ {
+ "name": "Test Property Garden",
+ "description": "property with garden",
+ "expected_price": 200000,
+ "living_area": 100,
+ },
+ ]
+ )
+
+ cls.offers = cls.env["estate.property.offer"].create(
+ [
+ {
+ "partner_id": cls.env.ref("base.res_partner_2").id,
+ "price": 110000,
+ "property_id": cls.properties[0].id,
+ },
+ {
+ "partner_id": cls.env.ref("base.res_partner_1").id,
+ "price": 120000,
+ "property_id": cls.properties[0].id,
+ },
+ {
+ "partner_id": cls.env.ref("base.res_partner_3").id,
+ "price": 125000,
+ "property_id": cls.properties[0].id,
+ },
+ ]
+ )
+
+ def test_property_sale(self):
+ with self.assertRaises(UserError):
+ self.properties[0].action_set_sold()
+
+ self.offers[1].action_accept_offer()
+
+ self.properties[0].action_set_sold()
+ self.assertEqual(self.properties[0].state, "sold", "Property was not sold")
+
+ with self.assertRaises(UserError):
+ self.env["estate.property.offer"].create(
+ {
+ "partner_id": self.env.ref("base.res_partner_4").id,
+ "price": 200000,
+ "property_id": self.properties[0].id,
+ }
+ )
+
+ def test_garden_reset(self):
+ with Form(self.properties[1]) as form:
+ form.garden = True
+ self.assertEqual(form.garden_area, 10)
+ self.assertEqual(form.garden_orientation, "north")
+
+ form.garden = False
+ self.assertEqual(form.garden_area, 0, "Garden area should be reset to 0")
+ self.assertEqual(
+ form.garden_orientation,
+ False,
+ "Garden orientation should be reset to False",
+ )
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..c2265bae90e
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..493caa3167d
--- /dev/null
+++ b/estate/views/estate_property_offer_views.xml
@@ -0,0 +1,41 @@
+
+
+
+ estate.property.offer.tree
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
+
diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml
new file mode 100644
index 00000000000..96184876b3d
--- /dev/null
+++ b/estate/views/estate_property_tags_views.xml
@@ -0,0 +1,34 @@
+
+
+
+ Property Tags
+ estate.property.tags
+ list,form
+
+
+
+
+ estate.property.tags.list
+ estate.property.tags
+
+
+
+
+
+
+
+
+
+ estate.property.tags.form
+ estate.property.tags
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..4ebd46b9b9b
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,60 @@
+
+
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+
+
+ Property Type Offers
+ estate.property.offer
+ list
+ [('property_type_id', '=', active_id)]
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..49c56412f8b
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,110 @@
+
+
+
+ Properties
+ estate.property
+ list,form
+ {'search_default_available_properties': True}
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml
new file mode 100644
index 00000000000..a18583359db
--- /dev/null
+++ b/estate/views/res_users_views.xml
@@ -0,0 +1,14 @@
+
+
+ res.users.view.form.inherit.estate
+ res.users
+
+
+
+
+
+
+
+
+
+
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/estate_account/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
new file mode 100644
index 00000000000..93dff616702
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,16 @@
+{
+ "name": "estate account",
+ "version": "0.1",
+ "depends": ["base", "estate", "account"],
+ "author": "Ayush Patel",
+ "category": "Real Estate",
+ "description": """
+ This module links Estate and Accounting.
+ """,
+ "application": True,
+ "auto_install": True,
+ "data": [
+ "reports/estate_account_templates.xml",
+ ],
+ "license": "LGPL-3",
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..5e1963c9d2f
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..513e8374866
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,31 @@
+from odoo import models, Command
+
+
+class EstateProperty(models.Model):
+ _inherit = "estate.property"
+
+ def action_set_sold(self):
+ self.check_access("write")
+ self.env["account.move"].sudo().create(
+ {
+ "partner_id": self.buyer_id.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create(
+ {
+ "name": f"Sale of property {self.name}",
+ "quantity": 1,
+ "price_unit": self.selling_price * 0.06,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Administrative Fees",
+ "quantity": 1,
+ "price_unit": 100.00,
+ }
+ ),
+ ],
+ }
+ )
+ return super().action_set_sold()
diff --git a/estate_account/reports/estate_account_templates.xml b/estate_account/reports/estate_account_templates.xml
new file mode 100644
index 00000000000..fa9eba70534
--- /dev/null
+++ b/estate_account/reports/estate_account_templates.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Invoice has already been created!!!
+
+
+
+
diff --git a/new_product_type/__init__.py b/new_product_type/__init__.py
new file mode 100644
index 00000000000..aee8895e7a3
--- /dev/null
+++ b/new_product_type/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizards
diff --git a/new_product_type/__manifest__.py b/new_product_type/__manifest__.py
new file mode 100644
index 00000000000..d8be7483028
--- /dev/null
+++ b/new_product_type/__manifest__.py
@@ -0,0 +1,14 @@
+{
+ "name": "New Kit Product",
+ "version": "1.0",
+ "depends": ["sale_management"],
+ "license": "LGPL-3",
+ "data": [
+ "security/ir.model.access.csv",
+ "views/product_template_views.xml",
+ "views/sale_order_views.xml",
+ "wizards/sub_products_wizard.xml",
+ ],
+ "installable": True,
+ "application": True,
+}
diff --git a/new_product_type/models/__init__.py b/new_product_type/models/__init__.py
new file mode 100644
index 00000000000..8f2f8c0cbc1
--- /dev/null
+++ b/new_product_type/models/__init__.py
@@ -0,0 +1,3 @@
+from . import product_template
+from . import sale_order_line
+from . import sale_order
diff --git a/new_product_type/models/product_template.py b/new_product_type/models/product_template.py
new file mode 100644
index 00000000000..d6d750f9d51
--- /dev/null
+++ b/new_product_type/models/product_template.py
@@ -0,0 +1,13 @@
+from odoo import models, fields
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ is_kit = fields.Boolean(string="Is Kit", default=False)
+ sub_products = fields.Many2many(
+ "product.product",
+ string="Sub Products",
+ help="Select the products that are part of this kit",
+ domain="[('is_kit', '=', False)]",
+ )
diff --git a/new_product_type/models/sale_order.py b/new_product_type/models/sale_order.py
new file mode 100644
index 00000000000..37a6f0466be
--- /dev/null
+++ b/new_product_type/models/sale_order.py
@@ -0,0 +1,41 @@
+from odoo import api, fields, models
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+ print_in_report = fields.Boolean(
+ string="Print Sub-products in Report",
+ default=False,
+ help="If checked, the individual sub-product components will be printed on the quotation/order report.",
+ )
+
+ @api.onchange("order_line")
+ def _onchange_order_line(self):
+ current_kit_ids = [
+ line._origin.id for line in self.order_line if line.product_is_kit
+ ]
+
+ new_order_lines = self.order_line.filtered(
+ lambda line: not line.parent_kit_line_id.id
+ or (line.parent_kit_line_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: not line.parent_kit_line_id)
+
+ def _get_invoiceable_lines(self, final=False):
+ invoicable_lines = super()._get_invoiceable_lines(final=final)
+ print(len(invoicable_lines), "invoicable lines before filter")
+ if self.print_in_report:
+ print(len(invoicable_lines), "invoicable lines after filter if true")
+ return invoicable_lines
+ else:
+ print(len(invoicable_lines), "invoicable lines after filter")
+ return invoicable_lines.filtered(lambda line: not line.parent_kit_line_id)
diff --git a/new_product_type/models/sale_order_line.py b/new_product_type/models/sale_order_line.py
new file mode 100644
index 00000000000..86c43d28291
--- /dev/null
+++ b/new_product_type/models/sale_order_line.py
@@ -0,0 +1,32 @@
+from odoo import api, fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ product_is_kit = fields.Boolean(
+ related="product_id.product_tmpl_id.is_kit",
+ )
+
+ parent_kit_line_id = fields.Many2one(
+ "sale.order.line",
+ string="Parent Kit Line",
+ ondelete="cascade",
+ copy=False,
+ )
+
+ sub_product_line_ids = fields.One2many(
+ "sale.order.line", "parent_kit_line_id", string="Sub-product Lines", copy=False
+ )
+
+ is_kit_sub_product = fields.Boolean(string="Is a Kit Sub-product", copy=False)
+
+ def open_sub_product_wizard(self):
+ return {
+ "name": f"Product : {self.product_id.display_name}",
+ "type": "ir.actions.act_window",
+ "res_model": "sub.products.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {"active_id": self.id},
+ }
diff --git a/new_product_type/security/ir.model.access.csv b/new_product_type/security/ir.model.access.csv
new file mode 100644
index 00000000000..6a08720d626
--- /dev/null
+++ b/new_product_type/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_sub_products_wizard_user,access.sub.products.wizard.user,model_sub_products_wizard,base.group_user,1,1,1,1
+access_sub_products_line_wizard_user,access.sub.products.line.wizard.user,model_sub_products_line_wizard,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/new_product_type/views/product_template_views.xml b/new_product_type/views/product_template_views.xml
new file mode 100644
index 00000000000..dcd8d4f59e8
--- /dev/null
+++ b/new_product_type/views/product_template_views.xml
@@ -0,0 +1,13 @@
+
+
+ product.template.form.kit.inherit
+ product.template
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/new_product_type/views/sale_order_views.xml b/new_product_type/views/sale_order_views.xml
new file mode 100644
index 00000000000..794e87a314c
--- /dev/null
+++ b/new_product_type/views/sale_order_views.xml
@@ -0,0 +1,40 @@
+
+
+
+ sale.order.form.inherited
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/new_product_type/wizards/__init__.py b/new_product_type/wizards/__init__.py
new file mode 100644
index 00000000000..da398f90783
--- /dev/null
+++ b/new_product_type/wizards/__init__.py
@@ -0,0 +1,2 @@
+from . import sub_products_wizard
+from . import sub_products_line
diff --git a/new_product_type/wizards/sub_products_line.py b/new_product_type/wizards/sub_products_line.py
new file mode 100644
index 00000000000..30f901f0a5f
--- /dev/null
+++ b/new_product_type/wizards/sub_products_line.py
@@ -0,0 +1,28 @@
+from odoo import api, fields, models
+
+
+class SubProductsLineWizard(models.TransientModel):
+ _name = "sub.products.line.wizard"
+
+ product_id = fields.Many2one(
+ "product.product",
+ string="Product",
+ required=True,
+ help="The product for which sub-products are being selected",
+ )
+ quantity = fields.Float(
+ string="Quantity",
+ required=True,
+ default=1.0,
+ help="The quantity of the sub-product to be added",
+ )
+ price = fields.Float(
+ string="Price", required=True, help="The price of the sub-product"
+ )
+ sub_products_wizard_id = fields.Many2one(
+ "sub.products.wizard",
+ string="Sub Products Wizard",
+ required=True,
+ ondelete="cascade",
+ help="The wizard from which this line is being created",
+ )
diff --git a/new_product_type/wizards/sub_products_wizard.py b/new_product_type/wizards/sub_products_wizard.py
new file mode 100644
index 00000000000..ec2ca8c0353
--- /dev/null
+++ b/new_product_type/wizards/sub_products_wizard.py
@@ -0,0 +1,109 @@
+from odoo import api, fields, models
+
+
+class SubProductsWizard(models.TransientModel):
+ _name = "sub.products.wizard"
+ _description = "Wizard to Configure Kit Sub-Products"
+
+ order_line_id = fields.Many2one(
+ "sale.order.line",
+ string="Sale Order Line",
+ required=True,
+ readonly=True,
+ help="The main sale order line for the kit product.",
+ )
+ order_id = fields.Many2one(
+ related="order_line_id.order_id",
+ string="Sale Order",
+ )
+ product_id = fields.Many2one(
+ related="order_line_id.product_id",
+ string="Kit Product",
+ )
+
+ sub_product_line_ids = fields.One2many(
+ "sub.products.line.wizard",
+ "sub_products_wizard_id",
+ string="Sub-Products",
+ )
+
+ total_price = fields.Float(
+ string="Total Kit Price",
+ compute="_compute_total_price",
+ digits="Product Price",
+ help="The final price of the main kit product based on the components.",
+ )
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ if self.env.context.get("active_id"):
+ order_line = self.env["sale.order.line"].browse(
+ self.env.context.get("active_id")
+ )
+ res["order_line_id"] = order_line.id
+
+ default_sub_products = order_line.product_id.sub_products
+
+ existing_sub_lines_map = {
+ line.product_id: line for line in order_line.sub_product_line_ids
+ }
+
+ wizard_lines = []
+ for sub_product in default_sub_products:
+ existing_line = existing_sub_lines_map.get(sub_product)
+ if existing_line:
+ quantity = existing_line.product_uom_qty
+ price = (
+ existing_line.price_unit
+ if existing_line.price_unit > 0
+ else sub_product.lst_price
+ )
+ else:
+ quantity = 1.0
+ price = sub_product.lst_price
+
+ wizard_lines.append(
+ (
+ 0,
+ 0,
+ {
+ "product_id": sub_product.id,
+ "quantity": quantity,
+ "price": price,
+ },
+ )
+ )
+
+ res["sub_product_line_ids"] = wizard_lines
+ return res
+
+ def action_confirm(self):
+ self.ensure_one()
+
+ self.order_line_id.sub_product_line_ids.unlink()
+
+ new_lines_vals = []
+ for line in self.sub_product_line_ids:
+ new_lines_vals.append(
+ {
+ "order_id": self.order_id.id,
+ "product_id": line.product_id.id,
+ "product_uom_qty": line.quantity,
+ "price_unit": 0,
+ "parent_kit_line_id": self.order_line_id.id,
+ "is_kit_sub_product": True,
+ }
+ )
+ self.env["sale.order.line"].create(new_lines_vals)
+
+ self.order_line_id.price_unit = self.total_price
+
+ return {"type": "ir.actions.act_window_close"}
+
+ @api.depends("sub_product_line_ids.quantity", "sub_product_line_ids.price")
+ def _compute_total_price(self):
+ for wizard in self:
+ wizard.total_price = sum(
+ line.quantity * line.price for line in wizard.sub_product_line_ids
+ )
diff --git a/new_product_type/wizards/sub_products_wizard.xml b/new_product_type/wizards/sub_products_wizard.xml
new file mode 100644
index 00000000000..0513d96b95c
--- /dev/null
+++ b/new_product_type/wizards/sub_products_wizard.xml
@@ -0,0 +1,32 @@
+
+
+
+ sub.products.wizard
+ sub.products.wizard
+
+
+
+
+
\ No newline at end of file
diff --git a/odoo_self_order_details/__init__.py b/odoo_self_order_details/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/odoo_self_order_details/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/odoo_self_order_details/__manifest__.py b/odoo_self_order_details/__manifest__.py
new file mode 100644
index 00000000000..c044ed1ea18
--- /dev/null
+++ b/odoo_self_order_details/__manifest__.py
@@ -0,0 +1,20 @@
+{
+ "name": "Self Order Details",
+ "description": """
+ Self order details for products in POS
+ """,
+ "author": "Ayush Patel",
+ "version": "0.1",
+ "application": True,
+ "installable": True,
+ "depends": ["pos_self_order"],
+ "license": "LGPL-3",
+ "assets": {
+ "pos_self_order.assets": [
+ "odoo_self_order_details/static/src/**/*",
+ ],
+ },
+ "data": [
+ "views/product_template_view.xml",
+ ],
+}
diff --git a/odoo_self_order_details/models/__init__.py b/odoo_self_order_details/models/__init__.py
new file mode 100644
index 00000000000..e8fa8f6bf1e
--- /dev/null
+++ b/odoo_self_order_details/models/__init__.py
@@ -0,0 +1 @@
+from . import product_template
diff --git a/odoo_self_order_details/models/product_template.py b/odoo_self_order_details/models/product_template.py
new file mode 100644
index 00000000000..d9ad6e34cae
--- /dev/null
+++ b/odoo_self_order_details/models/product_template.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ self_order_description = fields.Html(string="Self-Order Description")
diff --git a/odoo_self_order_details/static/src/product_card/product_card.js b/odoo_self_order_details/static/src/product_card/product_card.js
new file mode 100644
index 00000000000..10f2fc60170
--- /dev/null
+++ b/odoo_self_order_details/static/src/product_card/product_card.js
@@ -0,0 +1,48 @@
+import { patch } from "@web/core/utils/patch";
+import { ProductCard } from "@pos_self_order/app/components/product_card/product_card";
+import { ProductPage } from "@pos_self_order/app/pages/product_page/product_page";
+import { markup } from "@odoo/owl";
+
+// Patch ProductCard to always navigate to the product page on selection,
+// enabling display of self_order_description and large image for all products.
+patch(ProductCard.prototype, {
+ async selectProduct(qty = 1) {
+ const product = this.props.product;
+
+ if (!product.self_order_available || !this.isAvailable) {
+ return;
+ }
+
+ // For combo products, we use the default behavior
+ if (product.isCombo()) {
+ return super.selectProduct(qty);
+ }
+
+ // For other products, navigate to the product page
+ this.router.navigate("product", { id: product.id });
+ }
+});
+
+// Patch ProductPage component to fetch and display self_order_description
+patch(ProductPage.prototype, {
+ async setup() {
+ // call the original setup method to ensure the component is initialized properly
+ super.setup();
+
+ // This ensures that the product's self_order_description is fetched
+ const product = this.props.product;
+ if (product && !product.self_order_description) {
+ try {
+ const orm = this.env.services.orm;
+ // orm.read() returns all fields of product.product, including those added by other modules via _inherit = "product.product".
+ const [record] = await orm.read("product.product",[product.id]);
+ if (record && record.self_order_description) {
+ // markup is used to safely render HTML content
+ product.self_order_description = markup(record.self_order_description);
+ }
+ } catch (err) {
+ console.error("Failed to fetch self_order_description via ORM:", err);
+ }
+ }
+ },
+});
diff --git a/odoo_self_order_details/static/src/product_card/product_card.xml b/odoo_self_order_details/static/src/product_card/product_card.xml
new file mode 100644
index 00000000000..c1471ad6f4b
--- /dev/null
+++ b/odoo_self_order_details/static/src/product_card/product_card.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ options
+
+
+
+
+
+
+
![Product image]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odoo_self_order_details/views/product_template_view.xml b/odoo_self_order_details/views/product_template_view.xml
new file mode 100644
index 00000000000..aff114f9b2e
--- /dev/null
+++ b/odoo_self_order_details/views/product_template_view.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ product.template.form.inherit.self.order
+ product.template
+
+
+
+
+
+
+
+
+
diff --git a/purchase_order_print/__init__.py b/purchase_order_print/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/purchase_order_print/__manifest__.py b/purchase_order_print/__manifest__.py
new file mode 100644
index 00000000000..00c4f037fbc
--- /dev/null
+++ b/purchase_order_print/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ "name": "Purchase Order Print",
+ "version": "1.0",
+ "summary": "Custom module to print purchase orders with additional fields.",
+ "author": "Ayush Patel",
+ "depends": ["purchase", "base", "hr"],
+ "license": "LGPL-3",
+ "data": [
+ "reports/custom_purchase_report_template.xml",
+ "views/hr_employee_model_fields.xml",
+ "views/hr_employee_views.xml",
+ "views/product_model_fields.xml",
+ "views/product_template_views.xml",
+ "views/purchase_order_model_fields.xml",
+ "views/purchase_order_views.xml",
+ ],
+ "installable": True,
+ "application": True,
+}
\ No newline at end of file
diff --git a/purchase_order_print/reports/custom_purchase_report_template.xml b/purchase_order_print/reports/custom_purchase_report_template.xml
new file mode 100644
index 00000000000..3ee5e14f1e1
--- /dev/null
+++ b/purchase_order_print/reports/custom_purchase_report_template.xml
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
PURCHASE ORDER
+
+ F-Admin-002
+
+
+
+
+
+
+ ![]()
+
+ |
+
+
+ Purchase Order No.:
+ Date:
+
+ |
+
+
+
+ Billing/Shipping Address:
+
+
+ ,
+ ,
+
+ Phone:
+ |
+
+
+ GSTIN No :
+ CIN :
+ PAN No. :
+
+ |
+
+
+
+
+ Name of Vendor:
+
+
+ |
+
+
+
+
+ Vendor address :
+ GSTIN No :
+ PAN No. :
+
+ |
+
+
+ Quotation Ref No. :
+ Contact Person :
+ Contact No. :
+ Email Id :
+
+ |
+
+
+
+
+
+
+
+ Sr. No. |
+ MPN & Description |
+ Quantity |
+ UOM |
+ Rate/Unit |
+ Amount in INR |
+
+
+
+
+
+
+
+
+ |
+
+ -
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ Basic Total |
+
+
+ |
+
+
+ IGST |
+
+
+ |
+
+
+
+ Packing |
+
+ --
+ |
+
+
+
+ Freight |
+
+ --
+ |
+
+
+
+ Total PO Value in INR
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
Commercial terms & Conditions:
+
+
Payment Terms:
+
Delivery Schedules: Expected arrival date
+
Other terms & conditions:
+
+
General Terms
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
Annexure I
+
+
+
+
+
Authorised Distributor / OEM Name and Address:
+
+
+
+
+
+
+
+
+
Customer's Name and Address:
+
+
+
+
+
+
+
+
+ Date: ................................
+
+
+ CERTIFICATE OF CONFORMANCE
+
+
+
+
+ PO No. .................................... Date: .................................
+
+
+
+
+ INVOICE NO. ................................ Date: ................................
+
+
+
+
+
+ Sr. No. |
+ Make |
+ MPN (Part Number) |
+ Device type / Components / Description |
+ Quantity |
+ Lot No./Batch / Code * |
+ Date Code * |
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+ Authorised Signature (with date) and seal
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/purchase_order_print/views/hr_employee_model_fields.xml b/purchase_order_print/views/hr_employee_model_fields.xml
new file mode 100644
index 00000000000..7156b717b24
--- /dev/null
+++ b/purchase_order_print/views/hr_employee_model_fields.xml
@@ -0,0 +1,11 @@
+
+
+
+ x_signature_seal_image
+ hr.employee
+
+ Signature/Seal Image
+ binary
+ True
+
+
diff --git a/purchase_order_print/views/hr_employee_views.xml b/purchase_order_print/views/hr_employee_views.xml
new file mode 100644
index 00000000000..35c19beb9a0
--- /dev/null
+++ b/purchase_order_print/views/hr_employee_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ hr.employee.form.inherit.signature.seal
+ hr.employee
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/purchase_order_print/views/product_model_fields.xml b/purchase_order_print/views/product_model_fields.xml
new file mode 100644
index 00000000000..25c4c461e1a
--- /dev/null
+++ b/purchase_order_print/views/product_model_fields.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ x_part_no
+ product.template
+
+ char
+ Part No.
+ manual
+
+
+
+ x_packing
+ product.template
+
+ float
+ Packing
+ manual
+
+
+
+ x_freight
+ product.template
+
+ float
+ Freight
+ manual
+
+
+
\ No newline at end of file
diff --git a/purchase_order_print/views/product_template_views.xml b/purchase_order_print/views/product_template_views.xml
new file mode 100644
index 00000000000..85f43cdba92
--- /dev/null
+++ b/purchase_order_print/views/product_template_views.xml
@@ -0,0 +1,16 @@
+
+
+ product.template.form.inherit.partno.packing.freight
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/purchase_order_print/views/purchase_order_model_fields.xml b/purchase_order_print/views/purchase_order_model_fields.xml
new file mode 100644
index 00000000000..a13915bfbaa
--- /dev/null
+++ b/purchase_order_print/views/purchase_order_model_fields.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ x_employee_id
+
+ many2one
+ hr.employee
+ Authorized By
+ manual
+
+
+ x_annexure_terms
+
+ html
+ Annexure terms
+ manual
+
+
+
\ No newline at end of file
diff --git a/purchase_order_print/views/purchase_order_views.xml b/purchase_order_print/views/purchase_order_views.xml
new file mode 100644
index 00000000000..aa14a62acd9
--- /dev/null
+++ b/purchase_order_print/views/purchase_order_views.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ purchase.order.form.inherit.employee
+ purchase.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sale_person/__init__.py b/sale_person/__init__.py
new file mode 100644
index 00000000000..366188d46b3
--- /dev/null
+++ b/sale_person/__init__.py
@@ -0,0 +1 @@
+# Odoo module marker
\ No newline at end of file
diff --git a/sale_person/__manifest__.py b/sale_person/__manifest__.py
new file mode 100644
index 00000000000..4d9c8cc1b35
--- /dev/null
+++ b/sale_person/__manifest__.py
@@ -0,0 +1,20 @@
+{
+ "name": "Sale Person Attendance",
+ "version": "1.0",
+ "summary": "Track salesperson attendance and customer visits.",
+ "author": "Ayush Patel",
+ "depends": ["base", "base_automation"],
+ "license": "LGPL-3",
+ "data": [
+ "views/tag_model_fields.xml",
+ "views/contact_model_fields.xml",
+ "views/sale_person_model_fields.xml",
+ "views/tag_views.xml",
+ "views/contact_views.xml",
+ "views/sale_person_views.xml",
+ "views/sale_person_menu.xml",
+ "security/ir.model.access.csv",
+ ],
+ "installable": True,
+ "application": True,
+}
diff --git a/sale_person/security/ir.model.access.csv b/sale_person/security/ir.model.access.csv
new file mode 100644
index 00000000000..52d25b5b373
--- /dev/null
+++ b/sale_person/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_x_contact,x_contact,model_x_contact,base.group_user,1,1,1,1
+access_x_sale_person,x_sale_person,model_x_sale_person,base.group_user,1,1,1,1
+access_x_sale_person_tag,x_sale_person_tag,model_x_sale_person_tag,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/sale_person/views/contact_model_fields.xml b/sale_person/views/contact_model_fields.xml
new file mode 100644
index 00000000000..546b39a127e
--- /dev/null
+++ b/sale_person/views/contact_model_fields.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Contact
+ x_contact
+ manual
+
+
+
+
+
+
+
+ x_name
+ char
+ Name
+
+
+
+
+
+ x_customer_type
+ char
+ Customer Type
+
+
+
+
+
+ x_city
+ char
+ City
+
+
+
+
+
+ x_area
+ char
+ Area
+
+
+
+
+
+ x_pin_code
+ char
+ Pin Code
+
+
\ No newline at end of file
diff --git a/sale_person/views/contact_views.xml b/sale_person/views/contact_views.xml
new file mode 100644
index 00000000000..7e5fafaeac2
--- /dev/null
+++ b/sale_person/views/contact_views.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ Contacts
+ x_contact
+ list,form
+
+
+
+
+ x.contact.form
+ x_contact
+
+
+
+
+
+
+
+ x.contact.list
+ x_contact
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sale_person/views/sale_person_menu.xml b/sale_person/views/sale_person_menu.xml
new file mode 100644
index 00000000000..f8744d13267
--- /dev/null
+++ b/sale_person/views/sale_person_menu.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sale_person/views/sale_person_model_fields.xml b/sale_person/views/sale_person_model_fields.xml
new file mode 100644
index 00000000000..fba72ac567c
--- /dev/null
+++ b/sale_person/views/sale_person_model_fields.xml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+ Sale Person
+ x_sale_person
+ manual
+
+
+
+
+
+
+ x_user_id
+
+ many2one
+ res.users
+ 1
+ restrict
+
+
+
+
+ x_check_in
+
+ datetime
+ Check-in Time
+
+
+
+
+ x_check_out
+
+ datetime
+ Check-out Time
+
+
+
+
+ x_customer_id
+
+ many2one
+ x_contact
+ 1
+ restrict
+ Customer
+
+
+
+
+ x_city
+
+ char
+ City
+ x_customer_id.x_city
+ 1
+
+
+
+ x_area
+
+ char
+ Area
+ x_customer_id.x_area
+ 1
+
+
+
+ x_pin_code
+
+ char
+ Pin Code
+ x_customer_id.x_pin_code
+ 1
+
+
+
+
+ x_agenda
+
+ char
+ Agenda
+
+
+
+
+ x_conversion_possibility
+
+ selection
+ [('high','High'),('moderate','Moderate'),('low','Low')]
+ Conversion Possibility
+
+
+
+
+ x_worked_hours
+
+ float
+ Worked Hours
+
+
+
+
+ x_tag_ids
+
+ many2many
+ x_sale_person_tag
+ Tags
+
+
+
+
+ x_checkin_location
+
+ char
+ Check-in Location
+
+
+
+
+ x_checkout_location
+
+ char
+ Check-out Location
+
+
+
+
+ Check Out
+
+ code
+
+for record in records:
+ if not record.x_check_out:
+ checkout_time = datetime.datetime.now()
+ vals = {
+ 'x_check_out': checkout_time,
+ 'x_checkout_location': 'Auto-detected location (Checkout)'
+ }
+ if record.x_check_in:
+ delta = checkout_time - record.x_check_in
+ vals['x_worked_hours'] = delta.total_seconds() / 3600.0
+ record.write(vals)
+
+
+
+
+
+ Auto Set Check In Time
+
+ code
+
+record.write(
+ {
+ 'x_check_in': datetime.datetime.now(),
+ 'x_checkin_location': 'Auto-detected location (checkin)'
+ }
+)
+
+
+
+
+
+ Set Check In Time on Create
+
+ on_change
+
+
+ 1
+
+
\ No newline at end of file
diff --git a/sale_person/views/sale_person_views.xml b/sale_person/views/sale_person_views.xml
new file mode 100644
index 00000000000..4c413d799c4
--- /dev/null
+++ b/sale_person/views/sale_person_views.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+ Sales Person Attendance
+ x_sale_person
+ list,form
+ {'default_x_user_id': uid}
+
+
+
+
+ x.sale.person.list
+ x_sale_person
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ x.sale.person.form
+ x_sale_person
+
+
+
+
+
\ No newline at end of file
diff --git a/sale_person/views/tag_model_fields.xml b/sale_person/views/tag_model_fields.xml
new file mode 100644
index 00000000000..0d3ff963d01
--- /dev/null
+++ b/sale_person/views/tag_model_fields.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ x_Sale Person Tag
+ x_sale_person_tag
+ manual
+
+
+
+
+
+
+ x_name
+
+ char
+ Tag Name
+ 1
+
+
+
+
+ x_color
+
+ char
+ Color
+
+
\ No newline at end of file
diff --git a/sale_person/views/tag_views.xml b/sale_person/views/tag_views.xml
new file mode 100644
index 00000000000..b9d575a56b7
--- /dev/null
+++ b/sale_person/views/tag_views.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Tags
+ x_sale_person_tag
+ list,form
+
+
+
+
+ x.sale.person.tag.form
+ x_sale_person_tag
+
+
+
+
+
+
+
+ x.sale.person.tag.list
+ x_sale_person_tag
+
+
+
+
+
+
+
+
\ No newline at end of file