odoo-addon-pms 16.0.1.1.0__py3-none-any.whl → 16.0.2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. odoo/addons/pms/README.rst +1 -1
  2. odoo/addons/pms/__manifest__.py +6 -5
  3. odoo/addons/pms/data/pms_data.xml +0 -11
  4. odoo/addons/pms/i18n/de.po +184 -315
  5. odoo/addons/pms/i18n/es.po +131 -308
  6. odoo/addons/pms/i18n/it.po +110 -310
  7. odoo/addons/pms/i18n/pms.pot +0 -176
  8. odoo/addons/pms/models/__init__.py +0 -2
  9. odoo/addons/pms/models/pms_checkin_partner.py +26 -322
  10. odoo/addons/pms/models/res_company.py +0 -6
  11. odoo/addons/pms/models/res_partner.py +0 -73
  12. odoo/addons/pms/static/description/index.html +1 -1
  13. odoo/addons/pms/tests/test_pms_checkin_partner.py +7 -445
  14. odoo/addons/pms/tests/test_pms_folio.py +0 -6
  15. odoo/addons/pms/tests/test_pms_reservation.py +7 -53
  16. odoo/addons/pms/tests/test_pms_reservation_line.py +0 -3
  17. odoo/addons/pms/views/pms_checkin_partner_views.xml +0 -21
  18. odoo/addons/pms/views/res_company_views.xml +0 -1
  19. odoo/addons/pms/views/res_partner_views.xml +0 -1
  20. {odoo_addon_pms-16.0.1.1.0.dist-info → odoo_addon_pms-16.0.2.1.0.dist-info}/METADATA +3 -3
  21. {odoo_addon_pms-16.0.1.1.0.dist-info → odoo_addon_pms-16.0.2.1.0.dist-info}/RECORD +23 -27
  22. odoo/addons/pms/models/res_partner_id_category.py +0 -14
  23. odoo/addons/pms/models/res_partner_id_number.py +0 -141
  24. odoo/addons/pms/views/res_partner_id_category_views.xml +0 -29
  25. odoo/addons/pms/views/res_partner_id_number_views.xml +0 -29
  26. {odoo_addon_pms-16.0.1.1.0.dist-info → odoo_addon_pms-16.0.2.1.0.dist-info}/WHEEL +0 -0
  27. {odoo_addon_pms-16.0.1.1.0.dist-info → odoo_addon_pms-16.0.2.1.0.dist-info}/top_level.txt +0 -0
@@ -5,12 +5,8 @@
5
5
  import json
6
6
  from datetime import datetime
7
7
 
8
- from dateutil.relativedelta import relativedelta
9
-
10
8
  from odoo import _, api, fields, models
11
- from odoo.exceptions import UserError, ValidationError
12
- from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
13
- from odoo.tools.safe_eval import safe_eval
9
+ from odoo.exceptions import ValidationError
14
10
 
15
11
 
16
12
  class PmsCheckinPartner(models.Model):
@@ -28,12 +24,9 @@ class PmsCheckinPartner(models.Model):
28
24
  partner_id = fields.Many2one(
29
25
  string="Partner",
30
26
  help="Partner associated with checkin partner",
31
- readonly=False,
32
27
  index=True,
33
- store=True,
34
28
  comodel_name="res.partner",
35
29
  domain="[('is_company', '=', False)]",
36
- compute="_compute_partner_id",
37
30
  )
38
31
  reservation_id = fields.Many2one(
39
32
  string="Reservation",
@@ -213,50 +206,6 @@ class PmsCheckinPartner(models.Model):
213
206
  compute="_compute_birth_date",
214
207
  inverse=lambda r: r._inverse_partner_fields("birthdate_date", "birthdate_date"),
215
208
  )
216
- document_number = fields.Char(
217
- help="Host document number",
218
- readonly=False,
219
- store=True,
220
- compute="_compute_document_number",
221
- )
222
- document_type = fields.Many2one(
223
- help="Select a valid document type",
224
- readonly=False,
225
- store=True,
226
- index=True,
227
- comodel_name="res.partner.id_category",
228
- compute="_compute_document_type",
229
- domain="['|', ('country_ids', '=', False),"
230
- " ('country_ids', 'in', document_country_id)]",
231
- )
232
- document_expedition_date = fields.Date(
233
- string="Expedition Date",
234
- help="Date on which document_type was issued",
235
- readonly=False,
236
- store=True,
237
- compute="_compute_document_expedition_date",
238
- )
239
-
240
- document_id = fields.Many2one(
241
- string="Document",
242
- help="Technical field",
243
- readonly=False,
244
- store=True,
245
- index=True,
246
- comodel_name="res.partner.id_number",
247
- compute="_compute_document_id",
248
- ondelete="restrict",
249
- )
250
-
251
- document_country_id = fields.Many2one(
252
- string="Document Country",
253
- help="Country of the document",
254
- comodel_name="res.country",
255
- compute="_compute_document_country_id",
256
- store=True,
257
- readonly=False,
258
- )
259
-
260
209
  partner_incongruences = fields.Char(
261
210
  help="indicates that some partner fields \
262
211
  on the checkin do not correspond to that of \
@@ -286,50 +235,6 @@ class PmsCheckinPartner(models.Model):
286
235
  if record.partner_id:
287
236
  record.partner_id[partner_field_name] = record[checkin_field_name]
288
237
 
289
- @api.depends("partner_id")
290
- def _compute_document_number(self):
291
- for record in self:
292
- if not record.document_number and record.partner_id.id_numbers:
293
- last_update_document = record.partner_id.id_numbers.filtered(
294
- lambda x, record=record: x.write_date
295
- == max(record.partner_id.id_numbers.mapped("write_date"))
296
- )
297
- if last_update_document and last_update_document[0].name:
298
- record.document_number = last_update_document[0].name
299
-
300
- @api.depends("partner_id")
301
- def _compute_document_type(self):
302
- for record in self:
303
- if not record.document_type and record.partner_id.id_numbers:
304
- last_update_document = record.partner_id.id_numbers.filtered(
305
- lambda x, record=record: x.write_date
306
- == max(record.partner_id.id_numbers.mapped("write_date"))
307
- )
308
- if last_update_document and last_update_document[0].category_id:
309
- record.document_type = last_update_document[0].category_id
310
-
311
- @api.depends("partner_id")
312
- def _compute_document_expedition_date(self):
313
- for record in self:
314
- if not record.document_expedition_date and record.partner_id.id_numbers:
315
- last_update_document = record.partner_id.id_numbers.filtered(
316
- lambda x, record=record: x.write_date
317
- == max(record.partner_id.id_numbers.mapped("write_date"))
318
- )
319
- if last_update_document and last_update_document[0].valid_from:
320
- record.document_expedition_date = last_update_document[0].valid_from
321
-
322
- @api.depends("partner_id")
323
- def _compute_document_country_id(self):
324
- for record in self:
325
- if not record.document_country_id and record.partner_id.id_numbers:
326
- last_update_document = record.partner_id.id_numbers.filtered(
327
- lambda x, record=record: x.write_date
328
- == max(record.partner_id.id_numbers.mapped("write_date"))
329
- )
330
- if last_update_document and last_update_document[0].country_id:
331
- record.document_country_id = last_update_document[0].country_id
332
-
333
238
  @api.depends("partner_id")
334
239
  def _compute_firstname(self):
335
240
  for record in self:
@@ -433,11 +338,7 @@ class PmsCheckinPartner(models.Model):
433
338
  record.state = "dummy"
434
339
  elif any(
435
340
  not getattr(record, field)
436
- for field in record._checkin_mandatory_fields(
437
- country=record.country_id,
438
- document_type=record.document_type,
439
- birthdate_date=record.birthdate_date,
440
- )
341
+ for field in record._checkin_mandatory_fields()
441
342
  ):
442
343
  record.state = "draft"
443
344
  else:
@@ -473,49 +374,6 @@ class PmsCheckinPartner(models.Model):
473
374
  elif not record.phone:
474
375
  record.phone = False
475
376
 
476
- @api.depends("partner_id")
477
- def _compute_document_id(self):
478
- for record in self:
479
- if record.partner_id:
480
- if (
481
- not record.document_id
482
- and record.document_number
483
- and record.document_type
484
- ):
485
- id_number_id = (
486
- self.sudo()
487
- .env["res.partner.id_number"]
488
- .search(
489
- [
490
- ("partner_id", "=", record.partner_id.id),
491
- ("name", "=", record.document_number),
492
- ("category_id", "=", record.document_type.id),
493
- ]
494
- )
495
- )
496
- if not id_number_id:
497
- document_vals = record.get_document_vals()
498
- id_number_id = self.env["res.partner.id_number"].create(
499
- document_vals
500
- )
501
-
502
- record.document_id = id_number_id
503
- else:
504
- record.document_id = False
505
-
506
- def get_document_vals(self):
507
- return {
508
- "name": self.document_number,
509
- "partner_id": self.partner_id.id,
510
- "category_id": self.document_type.id,
511
- "valid_from": self.document_expedition_date,
512
- "country_id": self.document_country_id.id,
513
- }
514
-
515
- @api.model
516
- def _get_compute_partner_id_field_names(self):
517
- return ["document_number", "document_type", "firstname", "lastname"]
518
-
519
377
  def _completed_partner_creation_fields(self):
520
378
  self.ensure_one()
521
379
  if self.firstname or self.lastname:
@@ -531,40 +389,6 @@ class PmsCheckinPartner(models.Model):
531
389
  "nationality_id": self.nationality_id.id,
532
390
  }
533
391
 
534
- @api.depends(lambda self: self._get_compute_partner_id_field_names())
535
- def _compute_partner_id(self):
536
- for record in self:
537
- if not record.partner_id:
538
- if record.document_number and record.document_type:
539
- partner = self._get_partner_by_document(
540
- record.document_number, record.document_type
541
- )
542
- if not partner:
543
- if self._completed_partner_creation_fields():
544
- partner_values = self._get_partner_create_vals()
545
- partner = (
546
- self.env["res.partner"]
547
- .with_context(avoid_document_restriction=True)
548
- .create(partner_values)
549
- )
550
- record.partner_id = partner
551
-
552
- @api.model
553
- def _get_partner_by_document(self, document_number, document_type):
554
- number = (
555
- self.sudo()
556
- .env["res.partner.id_number"]
557
- .search(
558
- [
559
- ("name", "=", document_number),
560
- ("category_id", "=", document_type.id),
561
- ]
562
- )
563
- )
564
- return (
565
- self.sudo().env["res.partner"].search([("id", "=", number.partner_id.id)])
566
- )
567
-
568
392
  @api.depends("email", "mobile")
569
393
  def _compute_possible_existing_customer_ids(self):
570
394
  for record in self:
@@ -658,77 +482,10 @@ class PmsCheckinPartner(models.Model):
658
482
  _("This guest is already registered in the room")
659
483
  )
660
484
 
661
- @api.constrains("document_number")
662
- def check_document_number(self):
663
- for record in self:
664
- if record.partner_id:
665
- for number in record.partner_id.id_numbers:
666
- if record.document_type == number.category_id:
667
- if record.document_number != number.name:
668
- raise ValidationError(_("Document_type has already exists"))
669
-
670
485
  def _validation_eval_context(self, id_number):
671
486
  self.ensure_one()
672
487
  return {"self": self, "id_number": id_number}
673
488
 
674
- @api.constrains("document_number", "document_type")
675
- def validate_id_number(self):
676
- """Validate the given ID number
677
- The method raises an odoo.exceptions.ValidationError if the eval of
678
- python validation code fails
679
- """
680
- for record in self:
681
- if record.document_number and record.document_type:
682
- id_number = self.env["res.partner.id_number"].new(
683
- {
684
- "name": record.document_number,
685
- "category_id": record.document_type,
686
- }
687
- )
688
- if (
689
- self.env.context.get("id_no_validate")
690
- or not record.document_type.validation_code
691
- ):
692
- return
693
- eval_context = record._validation_eval_context(id_number)
694
- try:
695
- safe_eval(
696
- record.document_type.validation_code,
697
- eval_context,
698
- mode="exec",
699
- nocopy=True,
700
- )
701
- except Exception as e:
702
- raise UserError(
703
- _(
704
- "Error when evaluating the id_category "
705
- "validation code:\n %(name)s \n(%(error)s)",
706
- name=self.name,
707
- error=e,
708
- )
709
- ) from e
710
- if eval_context.get("failed", False):
711
- raise ValidationError(
712
- _(
713
- "%(doc_number)s is not a valid %(doc_type)s identifier",
714
- doc_number=record.document_number,
715
- doc_type=record.document_type.name,
716
- )
717
- )
718
-
719
- @api.constrains("document_country_id", "document_type")
720
- def _check_document_country_id_document_type_consistence(self):
721
- for record in self:
722
- if record.document_country_id and record.document_type:
723
- if (
724
- record.document_type.country_ids
725
- and record.document_country_id
726
- not in record.document_type.country_ids
727
- ):
728
- raise ValidationError(
729
- _("Document type and country of document do not match")
730
- )
731
-
732
489
  @api.constrains("state_id", "country_id")
733
490
  def _check_state_id_country_id_consistence(self):
734
491
  for record in self:
@@ -761,6 +518,14 @@ class PmsCheckinPartner(models.Model):
761
518
  if not any(record.partner_id[field] for field in address_fields):
762
519
  record.partner_id.write(residence_vals)
763
520
 
521
+ def set_partner_id(self):
522
+ for record in self:
523
+ if not record.partner_id:
524
+ if record._completed_partner_creation_fields():
525
+ partner_values = record._get_partner_create_vals()
526
+ partner = self.env["res.partner"].create(partner_values)
527
+ record.partner_id = partner
528
+
764
529
  @api.model_create_multi
765
530
  def create(self, vals_list):
766
531
  records = self.env["pms.checkin.partner"]
@@ -795,6 +560,9 @@ class PmsCheckinPartner(models.Model):
795
560
  "check-in in this reservation"
796
561
  )
797
562
  )
563
+ records_without_partner = records.filtered(lambda r: not r.partner_id)
564
+ if records_without_partner:
565
+ records_without_partner.set_partner_id()
798
566
  records.set_partner_address()
799
567
  return records
800
568
 
@@ -805,6 +573,10 @@ class PmsCheckinPartner(models.Model):
805
573
  tourist_tax_services_cmds = reservation._compute_tourist_tax_lines()
806
574
  if tourist_tax_services_cmds:
807
575
  reservation.write({"service_ids": tourist_tax_services_cmds})
576
+ records_without_partner = self.filtered(lambda r: not r.partner_id)
577
+ if records_without_partner:
578
+ records_without_partner.set_partner_id()
579
+
808
580
  self.set_partner_address()
809
581
  return res
810
582
 
@@ -826,8 +598,6 @@ class PmsCheckinPartner(models.Model):
826
598
  "firstname",
827
599
  "lastname",
828
600
  "birthdate_date",
829
- "document_number",
830
- "document_expedition_date",
831
601
  "nationality_id",
832
602
  "street",
833
603
  "street2",
@@ -835,8 +605,6 @@ class PmsCheckinPartner(models.Model):
835
605
  "city",
836
606
  "country_id",
837
607
  "state_id",
838
- "document_country_id",
839
- "document_type",
840
608
  ]
841
609
  return manual_fields
842
610
 
@@ -846,12 +614,13 @@ class PmsCheckinPartner(models.Model):
846
614
  manual_fields.append("reservation_id.state")
847
615
  return manual_fields
848
616
 
849
- @api.model
850
- def _checkin_mandatory_fields(
851
- self, country=False, document_type=False, birthdate_date=False
852
- ):
853
- mandatory_fields = []
854
- return mandatory_fields
617
+ def _checkin_mandatory_fields(self):
618
+ """
619
+ Auxiliar method to return the mandatory fields for checkin.
620
+ It can be extended by modules that need to add more mandatory fields.
621
+ """
622
+ self.ensure_one()
623
+ return []
855
624
 
856
625
  @api.model
857
626
  def _checkin_partner_fields(self):
@@ -893,28 +662,6 @@ class PmsCheckinPartner(models.Model):
893
662
  checkin_vals[key] = value
894
663
  checkin.write(checkin_vals)
895
664
 
896
- @api.model
897
- def calculate_doc_type_expedition_date_from_validity_date(
898
- self, doc_type, doc_date, birthdate
899
- ):
900
- today = fields.datetime.today()
901
- datetime_doc_date = datetime.strptime(doc_date, DEFAULT_SERVER_DATE_FORMAT)
902
- if datetime_doc_date < today:
903
- return datetime_doc_date
904
- datetime_birthdate = datetime.strptime(birthdate, DEFAULT_SERVER_DATE_FORMAT)
905
- age = today.year - datetime_birthdate.year
906
-
907
- document_expedition_date = False
908
- if doc_type.code == "D" or doc_type.code == "P":
909
- if age < 30:
910
- document_expedition_date = datetime_doc_date - relativedelta(years=5)
911
- else:
912
- document_expedition_date = datetime_doc_date - relativedelta(years=10)
913
- if doc_type.code == "C":
914
- if age < 70:
915
- document_expedition_date = datetime_doc_date - relativedelta(years=10)
916
- return document_expedition_date
917
-
918
665
  def action_on_board(self):
919
666
  for record in self:
920
667
  if record.reservation_id.checkin > fields.Date.today():
@@ -923,7 +670,8 @@ class PmsCheckinPartner(models.Model):
923
670
  raise ValidationError(_("Its too late to checkin"))
924
671
 
925
672
  if any(
926
- not getattr(record, field) for field in self._checkin_mandatory_fields()
673
+ not getattr(record, field)
674
+ for field in record._checkin_mandatory_fields()
927
675
  ):
928
676
  raise ValidationError(_("Personal data is missing for check-in"))
929
677
  vals = {
@@ -984,50 +732,6 @@ class PmsCheckinPartner(models.Model):
984
732
  "context": ctx,
985
733
  }
986
734
 
987
- def _save_data_from_portal(self, values):
988
- checkin_partner = values.get("checkin_partner", "")
989
- values.pop("checkin_partner")
990
- values.pop("folio_access_token") if "folio_access_token" in values else None
991
- if values.get("nationality"):
992
- values.update({"nationality_id": int(values.get("nationality_id"))})
993
-
994
- doc_type = (
995
- self.sudo()
996
- .env["res.partner.id_category"]
997
- .browse(int(values.get("document_type")))
998
- )
999
- if values.get("document_type"):
1000
- values.update({"document_type": int(values.get("document_type"))})
1001
- if values.get("state_id"):
1002
- values.update({"state_id": int(values.get("state_id"))})
1003
- if values.get("country_id"):
1004
- values.update({"country_id": int(values.get("country_id"))})
1005
-
1006
- if values.get("document_expedition_date"):
1007
- values.update(
1008
- {
1009
- "document_expedition_date": datetime.strptime(
1010
- values.get("document_expedition_date"), "%d/%m/%Y"
1011
- ).strftime("%Y-%m-%d"),
1012
- "birthdate_date": datetime.strptime(
1013
- values.get("birthdate_date"), "%d/%m/%Y"
1014
- ).strftime("%Y-%m-%d"),
1015
- }
1016
- )
1017
- doc_date = values.get("document_expedition_date")
1018
- birthdate = values.get("birthdate_date")
1019
- document_expedition_date = (
1020
- self.calculate_doc_type_expedition_date_from_validity_date(
1021
- doc_type, doc_date, birthdate
1022
- )
1023
- )
1024
- values.update(
1025
- {
1026
- "document_expedition_date": document_expedition_date,
1027
- }
1028
- )
1029
- checkin_partner.sudo().write(values)
1030
-
1031
735
  def send_portal_invitation_email(self, invitation_firstname=None, email=None):
1032
736
  template = self.sudo().env.ref(
1033
737
  "pms.precheckin_invitation_email", raise_if_not_found=False
@@ -43,12 +43,6 @@ class ResCompany(models.Model):
43
43
  default="no",
44
44
  )
45
45
 
46
- document_partner_required = fields.Boolean(
47
- help="""If true, the partner document is required
48
- to create a new contact""",
49
- default=False,
50
- )
51
-
52
46
  cancel_penalty_product_id = fields.Many2one(
53
47
  string="Cancel penalty product",
54
48
  help="Product used to calculate the cancel penalty",
@@ -4,7 +4,6 @@
4
4
  import logging
5
5
 
6
6
  from odoo import _, api, fields, models
7
- from odoo.exceptions import ValidationError
8
7
 
9
8
  _logger = logging.getLogger(__name__)
10
9
 
@@ -261,78 +260,6 @@ class ResPartner(models.Model):
261
260
  # Template to be inherited by localization modules
262
261
  return True
263
262
 
264
- @api.model_create_multi
265
- def create(self, vals_list):
266
- for vals in vals_list:
267
- check_missing_document = self._check_document_partner_required(vals)
268
- if check_missing_document:
269
- raise ValidationError(_("A document identification is required"))
270
- return super().create(vals_list)
271
-
272
- def write(self, vals):
273
- check_missing_document = self._check_document_partner_required(
274
- vals, partners=self
275
- )
276
- if check_missing_document:
277
- # REVIEW: Deactivate this check for now, because it can generate problems
278
- # with other modules that update technical partner fields
279
- _logger.warning(
280
- _("Partner without document identification, update vals %s"), vals
281
- )
282
- # We only check if the vat or document_number is updated
283
- if "vat" in vals or "document_number" in vals:
284
- raise ValidationError(_("A document identification is required"))
285
- return super().write(vals)
286
-
287
- @api.model
288
- def _check_document_partner_required(self, vals, partners=False):
289
- company_ids = (
290
- self.env["res.company"].sudo().search([]).ids
291
- if (not partners or any([not partner.company_id for partner in partners]))
292
- else partners.mapped("company_id.id")
293
- )
294
- if not self.env.context.get("avoid_document_restriction") and any(
295
- [
296
- self.env["res.company"]
297
- .sudo()
298
- .browse(company_id)
299
- .document_partner_required
300
- for company_id in company_ids
301
- ]
302
- ):
303
- return self._missing_document(vals, partners)
304
- return False
305
-
306
- @api.model
307
- def _missing_document(self, vals, partners=False):
308
- # If not is a partner contact and not have vat,
309
- # then return missing document True
310
- if (
311
- not vals.get("parent_id")
312
- or (partners and any([not partner.parent_id for partner in partners]))
313
- ) and (
314
- vals.get("vat") is False
315
- or vals.get("vat") == ""
316
- or (
317
- "vat" not in vals
318
- and (
319
- any([not partner.vat for partner in partners]) if partners else True
320
- )
321
- )
322
- or vals.get("country_id") is False
323
- or vals.get("country_id") == ""
324
- or (
325
- "country_id" not in vals
326
- and (
327
- any([not partner.country_id for partner in partners])
328
- if partners
329
- else True
330
- )
331
- )
332
- ):
333
- return True
334
- return False
335
-
336
263
  @api.constrains("is_agency", "property_product_pricelist")
337
264
  def _check_agency_pricelist(self):
338
265
  if any(
@@ -372,7 +372,7 @@ ul.auto-toc {
372
372
  !! This file is generated by oca-gen-addon-readme !!
373
373
  !! changes will be overwritten. !!
374
374
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
375
- !! source digest: sha256:7a9133a071292495a65eeaa21c559b55701ed046c6b661f3728055fedda6f778
375
+ !! source digest: sha256:aad37def55422d9d18b819885a02cc7d3903c0e7203d81ba07c4c893243c9165
376
376
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
377
377
  <p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/pms/tree/16.0/pms"><img alt="OCA/pms" src="https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/pms-16-0/pms-16-0-pms"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/pms&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
378
378
  <p>This module is an all-in-one property management system (PMS) focused on medium-sized properties