mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
[IMP] pms: allow partial invoicing (#61)
* [IMP] pms: allow partial invoicing * [FIX] pms: precommit fixes * [FIX] pms: uncomment init test file
This commit is contained in:
@@ -700,7 +700,7 @@ class FolioSaleLine(models.Model):
|
|||||||
"""
|
"""
|
||||||
return new or old
|
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.
|
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
|
should be added to the returned invoice line
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
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
|
reservation = self.reservation_id
|
||||||
service = self.service_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 = {
|
res = {
|
||||||
"display_type": self.display_type,
|
"display_type": self.display_type,
|
||||||
"sequence": self.sequence,
|
"sequence": self.sequence,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"product_id": self.product_id.id,
|
"product_id": self.product_id.id,
|
||||||
"product_uom_id": self.product_uom.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,
|
"discount": self.discount,
|
||||||
"price_unit": self.price_unit,
|
"price_unit": self.price_unit,
|
||||||
"tax_ids": [(6, 0, self.tax_ids.ids)],
|
"tax_ids": [(6, 0, self.tax_ids.ids)],
|
||||||
@@ -727,7 +740,7 @@ class FolioSaleLine(models.Model):
|
|||||||
"folio_line_ids": [(6, 0, [self.id])],
|
"folio_line_ids": [(6, 0, [self.id])],
|
||||||
"reservation_ids": [(6, 0, reservation.ids)],
|
"reservation_ids": [(6, 0, reservation.ids)],
|
||||||
"service_ids": [(6, 0, service.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:
|
if optional_values:
|
||||||
res.update(optional_values)
|
res.update(optional_values)
|
||||||
|
|||||||
@@ -991,8 +991,9 @@ class PmsFolio(models.Model):
|
|||||||
If False, invoices are grouped by
|
If False, invoices are grouped by
|
||||||
(partner_invoice_id, currency)
|
(partner_invoice_id, currency)
|
||||||
:param final: if True, refunds will be generated if necessary
|
: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
|
: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):
|
if not self.env["account.move"].check_access_rights("create", False):
|
||||||
try:
|
try:
|
||||||
@@ -1000,11 +1001,16 @@ class PmsFolio(models.Model):
|
|||||||
self.check_access_rule("write")
|
self.check_access_rule("write")
|
||||||
except AccessError:
|
except AccessError:
|
||||||
return self.env["account.move"]
|
return self.env["account.move"]
|
||||||
|
|
||||||
# 1) Create invoices.
|
# 1) Create invoices.
|
||||||
if not lines_to_invoice:
|
if not lines_to_invoice:
|
||||||
lines_to_invoice = self.sale_line_ids
|
lines_to_invoice = dict()
|
||||||
invoice_vals_list = self.get_invoice_vals_list(final, lines_to_invoice)
|
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:
|
if not invoice_vals_list:
|
||||||
raise self._nothing_to_invoice_error()
|
raise self._nothing_to_invoice_error()
|
||||||
@@ -1127,7 +1133,7 @@ class PmsFolio(models.Model):
|
|||||||
# Invoice line values (keep only necessary sections).
|
# Invoice line values (keep only necessary sections).
|
||||||
invoice_lines_vals = []
|
invoice_lines_vals = []
|
||||||
for line in order.sale_line_ids.filtered(
|
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":
|
if line.display_type == "line_section":
|
||||||
current_section_vals = line._prepare_invoice_line(
|
current_section_vals = line._prepare_invoice_line(
|
||||||
@@ -1152,7 +1158,7 @@ class PmsFolio(models.Model):
|
|||||||
current_section_vals = None
|
current_section_vals = None
|
||||||
invoice_item_sequence += 1
|
invoice_item_sequence += 1
|
||||||
prepared_line = line._prepare_invoice_line(
|
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)
|
invoice_lines_vals.append(prepared_line)
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,4 @@ from . import test_pms_wizard_folio
|
|||||||
from . import test_pms_res_users
|
from . import test_pms_res_users
|
||||||
from . import test_pms_amenity
|
from . import test_pms_amenity
|
||||||
from . import test_pms_room
|
from . import test_pms_room
|
||||||
|
from . import test_pms_folio_invoice
|
||||||
|
|||||||
@@ -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):
|
def setUp(self):
|
||||||
super(TestPmsFolioInvoice, self).setUp()
|
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,
|
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
|
||||||
and the related amounts"""
|
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,
|
"""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)"""
|
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,
|
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
|
||||||
and the related amounts in a specific segment of services (qtys)"""
|
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,
|
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
|
||||||
and the related amounts with board service linked"""
|
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,
|
"""Test create and invoice from the Folio, and check qty invoice/to invoice,
|
||||||
and the grouped invoice lines by room type, by one
|
and the grouped invoice lines by room type, by one
|
||||||
line by unit prices/qty with nights"""
|
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 """
|
""" 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
|
"""Test invoice qith a way of downpaument and check dowpayment's
|
||||||
folio line is created and also check a total amount of invoice is
|
folio line is created and also check a total amount of invoice is
|
||||||
equal to a respective folio's total amount"""
|
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
|
"""Test create with a discount and check discount applied
|
||||||
on both Folio lines and an inovoice lines"""
|
on both Folio lines and an inovoice lines"""
|
||||||
|
|
||||||
def test_reinvoice(self):
|
def _test_reinvoice(self):
|
||||||
"""Test the compute reinvoice folio take into account
|
"""Test the compute reinvoice folio take into account
|
||||||
nights and services qty invoiced"""
|
nights and services qty invoiced"""
|
||||||
|
|||||||
@@ -285,4 +285,12 @@ class FolioAdvancePaymentInv(models.TransientModel):
|
|||||||
)
|
)
|
||||||
if not lines_to_invoice:
|
if not lines_to_invoice:
|
||||||
raise UserError(_("Nothing 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
|
||||||
|
|||||||
Reference in New Issue
Block a user