diff --git a/pms/models/folio_sale_line.py b/pms/models/folio_sale_line.py index fde1810a8..1f6852e0c 100644 --- a/pms/models/folio_sale_line.py +++ b/pms/models/folio_sale_line.py @@ -700,7 +700,7 @@ class FolioSaleLine(models.Model): """ return new or old - def _prepare_invoice_line(self, **optional_values): + def _prepare_invoice_line(self, qty=False, **optional_values): """ Prepare the dict of values to create the new invoice line for a folio sale line. @@ -709,16 +709,29 @@ class FolioSaleLine(models.Model): should be added to the returned invoice line """ self.ensure_one() + if (qty > self.qty_to_invoice or qty < 1) and not self.display_type: + raise ValueError( + _( + "The qty (%s) is wrong." % qty + + " The quantity pending to invoice is %s" % self.qty_to_invoice + ) + ) reservation = self.reservation_id service = self.service_id - reservation_lines = self.reservation_line_ids.filtered(lambda l: not l.invoiced) + reservation_lines = self.reservation_line_ids.filtered( + lambda l: not l.invoiced and l.reservation_id + ) + lines_to_invoice = list() + if self.reservation_id: + for i in range(0, int(qty)): + lines_to_invoice.append(reservation_lines[i].id) res = { "display_type": self.display_type, "sequence": self.sequence, "name": self.name, "product_id": self.product_id.id, "product_uom_id": self.product_uom.id, - "quantity": self.qty_to_invoice, + "quantity": qty if qty else self.qty_to_invoice, "discount": self.discount, "price_unit": self.price_unit, "tax_ids": [(6, 0, self.tax_ids.ids)], @@ -727,7 +740,7 @@ class FolioSaleLine(models.Model): "folio_line_ids": [(6, 0, [self.id])], "reservation_ids": [(6, 0, reservation.ids)], "service_ids": [(6, 0, service.ids)], - "reservation_line_ids": [(6, 0, reservation_lines.ids)], + "reservation_line_ids": [(6, 0, lines_to_invoice)], } if optional_values: res.update(optional_values) diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index f20da8d28..58829cd75 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -991,8 +991,9 @@ class PmsFolio(models.Model): If False, invoices are grouped by (partner_invoice_id, currency) :param final: if True, refunds will be generated if necessary + :param lines_to_invoice: invoice specific lines dict(key=id, value=qty). + if False, invoice all :returns: list of created invoices - :lines_to_invoice: invoice specific lines, if False, invoice all """ if not self.env["account.move"].check_access_rights("create", False): try: @@ -1000,11 +1001,16 @@ class PmsFolio(models.Model): self.check_access_rule("write") except AccessError: return self.env["account.move"] - # 1) Create invoices. if not lines_to_invoice: - lines_to_invoice = self.sale_line_ids - invoice_vals_list = self.get_invoice_vals_list(final, lines_to_invoice) + lines_to_invoice = dict() + for line in self.sale_line_ids: + lines_to_invoice[line.id] = ( + 0 if line.display_type else line.qty_to_invoice + ) + invoice_vals_list = self.get_invoice_vals_list( + final=final, lines_to_invoice=lines_to_invoice + ) if not invoice_vals_list: raise self._nothing_to_invoice_error() @@ -1127,7 +1133,7 @@ class PmsFolio(models.Model): # Invoice line values (keep only necessary sections). invoice_lines_vals = [] for line in order.sale_line_ids.filtered( - lambda l: l.id in lines_to_invoice.ids + lambda l: l.id in list(lines_to_invoice.keys()) ): if line.display_type == "line_section": current_section_vals = line._prepare_invoice_line( @@ -1152,7 +1158,7 @@ class PmsFolio(models.Model): current_section_vals = None invoice_item_sequence += 1 prepared_line = line._prepare_invoice_line( - sequence=invoice_item_sequence + sequence=invoice_item_sequence, qty=lines_to_invoice[line.id] ) invoice_lines_vals.append(prepared_line) diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index ae06865ac..7136aa1a6 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -33,3 +33,4 @@ from . import test_pms_wizard_folio from . import test_pms_res_users from . import test_pms_amenity from . import test_pms_room +from . import test_pms_folio_invoice diff --git a/pms/tests/test_pms_folio_invoice.py b/pms/tests/test_pms_folio_invoice.py index a0ff9045c..d5e3f6f01 100644 --- a/pms/tests/test_pms_folio_invoice.py +++ b/pms/tests/test_pms_folio_invoice.py @@ -1,43 +1,200 @@ -from odoo.tests.common import SavepointCase +import datetime + +from .common import TestHotel -class TestPmsFolioInvoice(SavepointCase): +class TestPmsFolioInvoice(TestHotel): def setUp(self): super(TestPmsFolioInvoice, self).setUp() - def test_invoice_folio(self): + def create_common_scenario(self): + # create a room type availability + self.room_type_availability = self.env[ + "pms.room.type.availability.plan" + ].create({"name": "Availability plan for TEST"}) + # create a property + self.property = self.env["pms.property"].create( + { + "name": "MY PMS TEST", + "company_id": self.env.ref("base.main_company").id, + "default_pricelist_id": self.env.ref("product.list0").id, + } + ) + # create room type class + self.room_type_class = self.env["pms.room.type.class"].create( + {"name": "Room", "code_class": "ROOM"} + ) + + # create room type + self.room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.property.id], + "name": "Double Test", + "code_type": "DBL_Test", + "class_id": self.room_type_class.id, + "price": 25, + } + ) + + # create rooms + self.room1 = self.env["pms.room"].create( + { + "pms_property_id": self.property.id, + "name": "Double 101", + "room_type_id": self.room_type_double.id, + "capacity": 2, + } + ) + + self.room2 = self.env["pms.room"].create( + { + "pms_property_id": self.property.id, + "name": "Double 102", + "room_type_id": self.room_type_double.id, + "capacity": 2, + } + ) + + self.room3 = self.env["pms.room"].create( + { + "pms_property_id": self.property.id, + "name": "Double 103", + "room_type_id": self.room_type_double.id, + "capacity": 2, + } + ) + self.demo_user = self.env.ref("base.user_admin") + + def test_invoice_full_folio(self): + # ARRANGE + self.create_common_scenario() + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + } + ) + r1.flush() + r1.folio_id.flush() + r1.folio_id.sale_line_ids.flush() + state_expected = "invoiced" + # ACT + r1.folio_id._create_invoices() + # ASSERT + self.assertEqual( + state_expected, + r1.folio_id.invoice_status, + "The status after a full invoice folio isn't correct", + ) + + def test_invoice_partial_folio_by_steps(self): + # ARRANGE + self.create_common_scenario() + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + } + ) + dict_lines = dict() + # qty to 1 to 1st folio sale line + dict_lines[ + r1.folio_id.sale_line_ids.filtered(lambda l: not l.display_type)[0].id + ] = 1 + r1.folio_id._create_invoices(lines_to_invoice=dict_lines) + + # test does not work without invalidating cache + self.env["account.move"].invalidate_cache() + + self.assertNotEqual( + "invoiced", + r1.folio_id.invoice_status, + "The status after a partial invoicing is not correct", + ) + + # qty to 2 to 1st folio sale line + dict_lines[ + r1.folio_id.sale_line_ids.filtered(lambda l: not l.display_type)[0].id + ] = 2 + r1.folio_id._create_invoices(lines_to_invoice=dict_lines) + + self.assertEqual( + "invoiced", + r1.folio_id.invoice_status, + "The status after an invoicing is not correct", + ) + + def test_invoice_partial_folio_wrong_qtys(self): + # ARRANGE + self.create_common_scenario() + r1 = self.env["pms.reservation"].create( + { + "pms_property_id": self.property.id, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=2), + "adults": 2, + "room_type_id": self.room_type_double.id, + "partner_id": self.env.ref("base.res_partner_12").id, + } + ) + tcs = [-1, 0, 3] + + for tc in tcs: + with self.subTest(k=tc): + with self.assertRaises(ValueError): + # ARRANGE + dict_lines = dict() + dict_lines[ + r1.folio_id.sale_line_ids.filtered( + lambda l: not l.display_type + )[0].id + ] = tc + r1.folio_id._create_invoices(lines_to_invoice=dict_lines) + # test does not work without invalidating cache + self.env["account.move"].invalidate_cache() + + # TODO: complete tests + def _test_invoice_folio(self): """Test create and invoice from the Folio, and check qty invoice/to invoice, and the related amounts""" - def test_invoice_by_days_folio(self): + def _test_invoice_by_days_folio(self): """Test create and invoice from the Folio, and check qty invoice/to invoice, and the related amounts in a specific segment of days (reservation lines)""" - def test_invoice_by_services_folio(self): + def _test_invoice_by_services_folio(self): """Test create and invoice from the Folio, and check qty invoice/to invoice, and the related amounts in a specific segment of services (qtys)""" - def test_invoice_board_service(self): + def _test_invoice_board_service(self): """Test create and invoice from the Folio, and check qty invoice/to invoice, and the related amounts with board service linked""" - def test_invoice_line_group_by_room_type_sections(self): + def _test_invoice_line_group_by_room_type_sections(self): """Test create and invoice from the Folio, and check qty invoice/to invoice, and the grouped invoice lines by room type, by one line by unit prices/qty with nights""" - def test_autoinvoice_folio(self): + def _test_autoinvoice_folio(self): """ Test create and invoice the cron by partner preconfig automation """ - def test_downpayment(self): + def _test_downpayment(self): """Test invoice qith a way of downpaument and check dowpayment's folio line is created and also check a total amount of invoice is equal to a respective folio's total amount""" - def test_invoice_with_discount(self): + def _test_invoice_with_discount(self): """Test create with a discount and check discount applied on both Folio lines and an inovoice lines""" - def test_reinvoice(self): + def _test_reinvoice(self): """Test the compute reinvoice folio take into account nights and services qty invoiced""" diff --git a/pms/wizards/folio_make_invoice_advance.py b/pms/wizards/folio_make_invoice_advance.py index 043c1d2c9..101db95d9 100644 --- a/pms/wizards/folio_make_invoice_advance.py +++ b/pms/wizards/folio_make_invoice_advance.py @@ -285,4 +285,12 @@ class FolioAdvancePaymentInv(models.TransientModel): ) if not lines_to_invoice: raise UserError(_("Nothing to invoice")) - return lines_to_invoice + + lines_dict = dict() + for line in lines_to_invoice: + if not line.display_type: + lines_dict[line.id] = line.qty_to_invoice + else: + lines_dict[line.id] = 0 + return lines_dict + # return lines_to_invoice