From c61bcc48b8d2c22547ea811d81452673d1dec881 Mon Sep 17 00:00:00 2001 From: "David Van Droogenbroeck (DROD)" Date: Thu, 24 Oct 2024 08:44:46 +0200 Subject: [PATCH] [ADD] estate: Finish up to chapter 10 (included) --- estate/__init__.py | 1 + estate/__manifest__.py | 23 +++ estate/demo/estate_demo.xml | 97 +++++++++++++ estate/models/__init__.py | 1 + estate/models/estate_property.py | 139 +++++++++++++++++++ estate/models/estate_property_infos.py | 42 ++++++ estate/models/estate_property_offer.py | 76 ++++++++++ estate/security/ir.model.access.csv | 5 + estate/static/description/icon.webp | Bin 0 -> 9674 bytes estate/views/estate_menu_views.xml | 51 +++++++ estate/views/estate_property_offer_views.xml | 27 ++++ estate/views/estate_property_views.xml | 123 ++++++++++++++++ estate/views/estate_settings_views.xml | 75 ++++++++++ 13 files changed, 660 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/demo/estate_demo.xml create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/models/estate_property_infos.py create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/static/description/icon.webp create mode 100644 estate/views/estate_menu_views.xml create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_views.xml create mode 100644 estate/views/estate_settings_views.xml diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /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 0000000000..5f5f859fc4 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +# A custom real estate module, created for learning purposes + +{ + 'name': '[TUTO] Estate', + 'summary': 'Track your real estate properties', + 'depends': [ + 'base_setup' + ], + 'data': [ + 'security/ir.model.access.csv', + + 'views/estate_property_views.xml', + 'views/estate_settings_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_menu_views.xml' + ], + 'installable': True, + 'application': True, + 'demo': [ + "demo/estate_demo.xml", + ], + 'auto_install': True +} diff --git a/estate/demo/estate_demo.xml b/estate/demo/estate_demo.xml new file mode 100644 index 0000000000..389e05122f --- /dev/null +++ b/estate/demo/estate_demo.xml @@ -0,0 +1,97 @@ + + + + + + + Appartment + + + House + + + + + cosy + + + renovated + + + renovation needed + + + + + 2000000 + + 12 + + + 200000 + + 10 + + + 220000 + + 25 + + + 260000 + + + + + + + Beach House + canceled + A brand new house by the beach + 8300 + + 2500000 + 0 + 6 + 1050 + 4 + True + True + 420 + south + + + + + + + City Apt + offer_accepted + A shitty appartment in Brussels + 1090 + + 250000 + 0 + 2 + 95 + 1 + False + False + 0 + + + + + + + + + Mass cancel + + + list + code + action = records.action_cancel() + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..a94838fc6d --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property_infos, estate_property, estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 0000000000..0b6d60b866 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,139 @@ +from odoo import api, exceptions, fields, models +from datetime import date +from dateutil.relativedelta import relativedelta + + +class Estate_Property(models.Model): + _name = "estate_property" + _description = "Estate properties" + _order = "id desc" + active = False + + name = fields.Char(required=True, string="Title") + + status = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + default="new", + readonly=True, + copy=False, + string="Sale State", + ) + + description = fields.Text(copy=False, string="Description") + + postcode = fields.Char( + help="Une adresse serait mieux mais bon...", string="Postcode" + ) + + date_availability = fields.Date( + default=(date.today() + relativedelta(months=+3)), + copy=False, + string="Available From", + ) + + expected_price = fields.Float( + default=0.0, required=True, copy=False, string="Expected Price" + ) + + selling_price = fields.Float(readonly=True, string="Selling Price") + + bedrooms = fields.Integer(default=2, string="Bedrooms") + + living_area = fields.Integer(string="Living Area (sqm)") + + facades = fields.Integer(default=2, string="Facades") + + garage = fields.Boolean(default=False, string="Garage") + + garden = fields.Boolean(default=False, string="Garden") + + garden_area = fields.Integer(default=0, string="Garden Area (sqm)") + + garden_orientation = fields.Selection( + [("north", "North"), ("south", "South"), ("west", "West"), ("east", "East")], + string="Garden Orientation", + ) + + type_id = fields.Many2one( + "estate_property_type", required=True, string="Property Type" + ) + + buyer = fields.Many2one("res.partner", copy=False, string="Buyer") + + salesperson = fields.Many2one( + "res.users", default=(lambda self: self.env.user), string="Salesman" + ) + + tag_ids = fields.Many2many("estate_property_tag", string="Property Tags") + + offer_ids = fields.One2many("estate_property_offer", "property_id", string="Offers") + + total_area = fields.Integer(compute="_compute_total_area", string="Total Area") + + best_offer = fields.Float( + compute="_compute_best_offer", default=0.0, string="Best Offer" + ) + + _sql_constraints = [ + ( + "check_positive_expected_price", + "CHECK(expected_price >= 0.0)", + "Expected Price should be a positive number.", + ), + ( + "check_positive_selling_price", + "CHECK(selling_price >= 0.0)", + "Selling Price should be a positive number.", + ), + ] + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids") + def _compute_best_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped("price")) + + @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 = "" + + @api.constrains("selling_price", "expected_price") + def _check_expected_vs_selling_price(self): + for record in self: + if (record.selling_price > 0.0) and ( + record.selling_price < 0.9 * record.expected_price + ): + raise exceptions.ValidationError( + r"Cannot sell for less than 90% of expected price." + ) + + def action_sold(self): + for record in self: + if record.status != "canceled": + record.status = "sold" + else: + raise exceptions.UserError("Canceled properties cannot be sold.") + return True + + def action_cancel(self): + for record in self: + if record.status != "sold": + record.status = "canceled" + else: + raise exceptions.UserError("Sold properties cannot be canceled.") + return True diff --git a/estate/models/estate_property_infos.py b/estate/models/estate_property_infos.py new file mode 100644 index 0000000000..f4521f1534 --- /dev/null +++ b/estate/models/estate_property_infos.py @@ -0,0 +1,42 @@ +from odoo import fields, models + + +class Estate_Property_Type(models.Model): + _name = "estate_property_type" + _description = "Estate property Types" + _order = "name" + + name = fields.Char( + required=True, + string="Type" + ) + + property_ids = fields.One2many( + "estate_property", + "type_id", + string="Estate Properties" + ) + + _sql_constraints = [ + ("check_unique_type", "UNIQUE(name)", "Property types must be unique.") + ] + + +class Estate_Property_Tag(models.Model): + _name = "estate_property_tag" + _description = "Estate property Tags" + _order = "name" + + name = fields.Char( + required=True, + string="Type" + ) + + property_estate_ids = fields.Many2many( + "estate_property", + string="Estate Properties" + ) + + _sql_constraints = [ + ("check_unique_tag", "UNIQUE(name)", "Property tags must be unique.") + ] diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 0000000000..9c44323807 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,76 @@ +from odoo import api, exceptions, fields, models +from datetime import date +from dateutil.relativedelta import relativedelta + + +class Estate_Property_Offer(models.Model): + _name = "estate_property_offer" + _description = "Estate Property Offers" + _order = "price desc" + + price = fields.Float( + string="Price" + ) + + status = fields.Selection( + [("accepted", "Accepted"), ("refused", "Refused")], + readonly=True, + copy=False, + string="Status" + ) + + partner_id = fields.Many2one( + 'res.partner', + required=True, + string="Partner" + ) + + property_id = fields.Many2one( + 'estate_property', + string="Property" + ) + + validity = fields.Integer( + default=7, + string="Validity (days)" + ) + + deadline = fields.Date( + compute="_compute_deadline", + copy=False, + string="Deadline" + ) + + _sql_constraints = [ + ("check_positive_price", "CHECK(price > 0.0)", "Offer Price should be a positive number (higher than 0).") + ] + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + record.deadline = date.today() + relativedelta(days=+record.validity) + + def _inverse_deadline(self): + for record in self: + record.validity = relativedelta(date.today(), record.deadline) + + def action_accept(self): + for record in self: + if not any(offer_status == "accepted" for offer_status in record.property_id.offer_ids.mapped("status")): + # Set values in the Property itself + record.property_id.selling_price = record.price + record.property_id.buyer = record.partner_id + + record.status = "accepted" + else: + raise exceptions.UserError("An offer has already been accepted.") + return True + + def action_refuse(self): + for record in self: + if record.status == "accepted": + # Set values in the Property itself + record.property_id.selling_price = 0.0 + record.property_id.buyer = None + record.status = "refused" + return True diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 0000000000..0db13e578e --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/static/description/icon.webp b/estate/static/description/icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..9758ad2b58947c7a4fe8634e9388946497b4dbe4 GIT binary patch literal 9674 zcmeI0RZtz=lJ7U}?i$<)5<+l?0KtR1Z`>uYad&swxVyVsa1tbJ+zA@oVc&Ca-Kslh z>YOt(b?0TOzJB?wRka>^b#?!{tJUOWWCrO004-?=6-^aBs@A`qF9=|BVOXXR`QSwo zWs4Nbii--VT-#+Ya8axsUL+vvJ1VNbHpmgGmkcr<`+mQg+pk<}JTspNm*M<|d4l;x zkuZ7f|HuEge~Bmk+wL>c!+T$hrTQ^z==a`huah;XxhSVQ+Dn<^yY6e`^ZS#wdgy>x z@*UDGbPMj*Xmodo^D@%k^hO+t6AWTKGCZ9bmN@Sg%^bf4GurLG2Y{>S_?~_m!WzJCM`xXEpnMvd5GX2U2G*R2x6w#_P z?D4)m`Sthk4z{r#(m=+;1wydDcA85*=Qe7eR<5A$Kk;wa?0pz`lm$>xZR|cPe3mQ1 zmt7?)usaev(XsFtBX^G?MJjuT^b)+y|A4R-DG;PhVeCw-IQ_)gPemF~O|%=*rGJE; zV2Gv8qt%D5SHmTK;5t~^gt%iPcKI|Pj!Dkvv_1597-(myBJ}~&PWF-m{PAa2NP=!O z(m+j2ra#IuCAVUPWziI;*LwlD>T#8_=-@`;kGLbbr~+Ghql!8JC`6zV-A6A51Al&( z#vetPRol>%n_>V1mK$fv5uSSf_2g#wa+SRnrpfNtnm( z=VVV2#rX=4j7=pL2>bVd+fHtrd74s394DhjyO_nUA9^{JRH8HvgDG`)X8Vk3^C=B? zd$9Wm9G~Jsx?2fHhfBJFBLS3GRmUJq%or zT~hsnMEKjM&%10WbSqmQ(rsiUWp5b+To{E`)kllge z5+%OCD(e={Fy(o^n=^<9^~27)*})xDd$x^~gwr$H_FpTaDeNfd!)-WOeDhyZ%UJw) zL;vxdQpl&Ja@)FR%VGm6%b-PiB@V_HU^UA$2pM~zm73)(+_}AuH7$WBLvYQMxofl*k(O^p-vZ3r;FBhfiAv8nzGE!6W%6 z=~lNiXogyD{xr&KmDH~&FU8&0fHg*XtSenTK#)BVF^g@Xpdt%OjfUyX-n}wDEP;4# z8;T1JvjskU>Te7^rB}4>bP^AP5TNu--HRY&i8g>h{?05@z?litKH(ecZGIhDK9C+x zv1T?0z2~qQoD?ZgDObSlXuj;*YccGP$_Y9|PnkxQ z*l!S^yf99oe0Vnt$m|1*RhEXVaI(ghJJ(U2n~=iwJT<13Gls@2<%&V|GdXy;TR-R@ z7kiz-uC@Z&nfk7E+J(DI&Ol3hJik6KNDn>0GB#a-PWAvUvreF5wwc-GxoZB{o3^@Qb?uO+{ObP^qv~4*;;{YyXn30%|9{#An)8(2`H!^x-|9R= zK}L!;e|a<^n|6cg$bSwbN!l4Cv0wD%(+J(aOw-=DgsiNq7bQB2nsE?o5B7dUE7tSqOU}Rl z$>w(!hyAAvrdyaM(B|O0F)rpFvo7&i50`h0K>j1#+B-Idg+VJ%8&%4=wJCTesS?q` zg`N1$i(+xYw8!q}L{Cx(*7HVJTqNn2-NLE-dmhBD)$G<1mfQKaHlW;2L665kR<1ADo;VljGUJFw z@7tDiu@+Hwg{bSDFO+H|)x?L21_CoJ$=vMN9=7~7R$D(98ie1C?i&3FEM?r55`uj3 zyl^khl9z*o2(f@j3u|#vKcX-2ecB00B|&~U^{}?0A0pb^Eu=Z*nC;NOkYs;en4=gu z|E0PFDs39vM5bp@3$1^-|MK*UVo~v0-jSBEWv@9heURhrC2_ecbnE9p#5aIwzvySQ z-6>+gt#qDG$=V=gtU#Wf!z5V{=g$z$g&b;lBbx2L!gpb%dvXQ56i!0F#b5&8svYZz zQmH}saJHobCn0|}Rb~|?Hzi_c^z7hQ4Rs{07gYhQ>~)&YyGI4s`CR&e@|iN9wFK=_ z{R@}_55t`g+b56wy6kVe@cOeH*<)2TdxPjlBb`MCmbe0=MZKly)6izR{OOaEN8Z)7 z{u%f0a60}F8nM53#jIrMi=6D@f<>MvRKEZd(62z#p`&DLjPbqI#)H5skMEC`m}H;I zYpt^g+ERmxVQMQdQ$iP+k>Q){FSPz>#MFXmS}B=mnk=Zi#ArG3x`t`x4b{C0hajA|vQMyO&sGZ1c9Oo;a}F`2gGGY#`KlZUC{ zhv=M>-uSB_&B;wNx0$c3TA`kVBmEHxXMOboioT_pIDgnCgvc!^MD3X1Ef^{^Xt}fodK?4kvmf2%M9;xFP(-F8aci1CR~E*WN+8gs%6%R* zV5RAfO0d^3Q1sF}r9>0HDpuDoj0`QJDUSEiUYpH2d>U3s?tn#6N`HY$8uP zUup5ZLlg z%3}oaU$Txj=1h7Mpl4Xw;!~okoLZjFF*%8??=W`sNsXN^sd_n`z$LCpyZ;X)H#|A;yQ$j zb?Wj?#@4})o`Zx(C36rAdS^PM`r|#A4 zPzn#vyL=Lsd%67Hguu{Um!tdab@b_-73+p^b0Sqc7{GG$mTX4hi{05$O%F zmt)_iJt^D|jXn_k#mUVa?TxA50zsXO9PyX)C+U{502C^0aqPFknNg-M8n-o${)U<< z#uQb24^oTP65;Y09AppmZ&fmV;9$1&!YJcKZV*)y(Ff%=q3M&7^)T=00$b=FT=tM@ z?~}6{4U5+M;Jm{iXQn!blY>w*m?n;;SMfh3gxjUn9xBx$3jr>^P;??Pbio~cz6*2L z(k1gs{iXqe?=&iuto^hKLY{f@X{2a;rVtOTg+~8h!ns>T;Jnfqoj(7)%IR-*+(whA z`;uhzmwLc4s#5)<3?pykO;BEC)p6*)++N$`peVa~6l`_%zH(gg)CPBzlnN2$vWRY& zC!ecCce+uYV1UNAE!E}v9J(yfh#tPE-$V6LV#5LJyX z5t!77z#BlgR9%`3daCfKv~=jO4G1mihqz=Ih^emJzIGB^$a@px4Le^ceoNxrmu+zm zdMBZ@@1ZBSpm7j zbxRA^ZuzSTb7c5Odw;nTciP$G@p~&1Kg3vhtM~61%F{fqdIcU2W+M zN(X|kH-ESoPCn}x*b%$=4d7bwU-%cH?H)~_mtRrqI{ zZR5)i`kHVA4m&M~75KlmrBKf+jgQ4Wo__&A=0^V$WSNR#Q)%XLl(%~=OGi! zb%t#b_o^djW$Zhb68Aau9O|}hX&HeFpwAuSp44;c!=;nAo_6jDCYf>>sgfO#yo$lp zJX7ZOzXU)+cItY51hYH2$C|}L~kc=ql5rH9DsWql13;EV0}5dP+{7e>ete?a7rvAJBtF73{7=P?jFuYzkSLF z6m4fO;AZ!Eab&C^IoITSJS4n=<5NPO-7RDIhWuW2k%h_gfc2-6UL-LOweWifKk;su zo^cf(pJ*>SCA`sBzc!Pk-G&c^=A$`9M;cEO{(PKPPbZhZ!xSWboZLND zu>f4$tjEWyJe9sMp!)+7Z!gC~I2`^ZvTcvpjT7P<5t}ac&?>%IGL+G6gB3J)*EsDZ z_F{5UHhJrODRo{6S--Az#k6cBm&7snR{9kc6+U;H7K-Ov+QJ>Qq&#ET=l{D?4481z z=`u<4ZX<=z&p^;GcCc)2VDL^W8}8Y8$b=*tGI_$=c6LCl5JTc1>^2RV{dm2Gf`Ej8 zO_LYY@ad+!8Dq*C@s814^a7o?zmHAglJ0KUi+b<#P8{N_l^SPTc;W1jr*)`aGY>3i zI%E{S+!h7d+a$;&&)@^(yEr*3)Y(?237wwacH^7uv10H7>pYDZ49!qs{`YwZkJ7{Cy2LHI0b2V1nNoCviEoAw|PBNQ1$Gw9)YpPm zs+qb99JR~-Xgz(%l9oMveBG0o(iR=?L8h8PDd*ou@mL2A4wbenCG?X=zjKoAS^OmF z{iAVV78t{49rd+Y|^hg~~>rAewMZqpd| zQMWzEKJNdMw9+ga{^Z^gCJKIE{qO z!})FU3e3gu-8xi+!2Nj~mLe4Z*b99k-buk(@f3rX8@q#};7>!0*Ns|(Uc&JG>*9J6 zOfcR_RmR3-S8nc29gX{0;XxSk`P%b!P>WuwEv&mtmBb{>x8{TZi{j`KC4A^2ibg6EM&nxbu8w&j>l0ji zHJj4_4R@5<&B==_qsnu3vIWi|h%uu^YWA=^#+u53eKr75vs;ti!iehk^p7J(jkm?< zsQn~uG_9NTyX!I3hPOQjp6Ab2Xc*h@OJ=AWaAfi4&OYF5(N5yexo)v9o0l*+t;@|B zeYRe9p7E%1-t|JM5h$WHa}GzR`mwI&ro{EadLuBK+@_8h@m-rNFswxl9>p5M_upY) zZ;dC_Dh=3gp^;bbmqiJ_tVAw7fiGgmYl&KQm?xs=@~}m_eY%KD(sR38ZZN`^!xap9 zL<(g@K1z4CrcuN=hM}C-p;Zv_qn~S7b1?Q>+oqycB~1=EF#LE3@T!afOeM&d!vgt0ghq0+y*O2PBz+{9(1T>5ixuVpPsW9rq!zG zZ!Szcp1AaJ%ievbc(gXQod~kE%6zbOF-`Tabz~Epv5XiN>h)+{_|dr&10EViZhl7M zC7P4@J;NBuy3;g@F%);D0Bjg(TaQC3RhzD0dmQjpK2rAp(Y?*6F>6+fly=!XV4mHl z`1>lnQ?v>$yb7it(Wv2Sn0xGItJds2(>gq!HiL!=h8P3#8;wAeFjP|!xQcf8=8yXH zP&Wl%+#?YpcK6JT&Hdu8y)dMvKQBXaSV?{+zjM5L1j3%9r!LC|LHp_rsutwak=`+ zn9636qpz;kw)yuj2{>a)eUkV<+XUwo?FSa>c0VT>8a(f1eD0wA+(-uS{H=U$W;GqV zBR9YNv+q-`7&J9@3+e3_sR18241N{Dy2`lz1}DPexYejovpHpr6Onn%L&b$gu{bM!2DTET6{DGK zahL=o&?Jg|(QKL)&z=*&zBDZr`2BW1#XGts`rxC;;D4^fZ(ILc+PISJ)1M_w_fwe5 z8)cF84 zN#C8{8<4T8bcd7F*2ER_$N^R%#D{&ihRJ#B7Hn1`vuV{Ys*igylj|wj_wg)Bvt&TN z0!#Jo^e0O?%t_vkwc)oUKXA+CS=?<+vs@X!M> za79^}zBbc^qA^i`5%yq2w?gxWBtB#F?26jzCV!!@lM*wd9Dn4Os^Mu#`|c|T)b9vA z)Kq&zpZtWc3op?A$XE|m#Y!G-4XPo<`aGIA4prAQ=6}oY;X1mL8rYgOm)^;K>sul| zq0X*T4lqQSEQEE+jG#Ff&!qG7wN2j?kk`|k1J{$RBFt-a19Wc^?Zt`EGle=1MN%)X z;FKt>qAwV?(oVZDPWYwU1?_@$&Pp8i&t2|-ZX|`nym|d>w&q|Rcq+9=GBiWU9>Dk# zYPPd)8IUVYLgN?7VRE7+7*8|;TZaVSH2;VR^|p#P`<0q@|q^VVd7GMHsLZAiAcI`R8{$@kx#(ic8r*e003%>;JCIl zzjIJt004W9E+jOfv8=}SwJMeCr(>i*FTU+^3 + + + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 0000000000..5599dbeae3 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,27 @@ + + + + + + estate_property_offer_list + estate_property_offer + + + + + + +