mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
14.0 pms account flow (#36)
* [WIP] Basic tests definition * [DEL] Default diff invoicing * [WIP] Reservation refact invoice fields * [FIX] test price without taxes * [FIX] Security csv merge * [WIP]pms: Wizard adv inv views * [ADD] Wizard Filter Invoice Days * [WIP] Payment WorkFlow
This commit is contained in:
@@ -35,6 +35,8 @@
|
||||
# "templates/pms_email_template.xml",
|
||||
"views/general.xml",
|
||||
"data/menus.xml",
|
||||
"wizards/wizard_payment_folio.xml",
|
||||
"wizards/folio_make_invoice_advance_views.xml",
|
||||
"views/pms_amenity_views.xml",
|
||||
"views/pms_amenity_type_views.xml",
|
||||
"views/pms_board_service_views.xml",
|
||||
@@ -50,6 +52,7 @@
|
||||
"views/pms_room_closure_reason_views.xml",
|
||||
"views/account_payment_views.xml",
|
||||
"views/account_move_views.xml",
|
||||
"views/account_bank_statement_views.xml",
|
||||
"views/res_users_views.xml",
|
||||
"views/pms_room_type_class_views.xml",
|
||||
"views/pms_room_type_availability_plan_views.xml",
|
||||
@@ -64,10 +67,12 @@
|
||||
"views/product_template_views.xml",
|
||||
"views/webclient_templates.xml",
|
||||
"views/ir_sequence_views.xml",
|
||||
"views/account_journal_views.xml",
|
||||
"wizards/wizard_reservation.xml",
|
||||
"wizards/wizard_massive_changes.xml",
|
||||
"wizards/wizard_advanced_filters.xml",
|
||||
"wizards/wizard_folio.xml",
|
||||
"wizards/wizard_invoice_filter_days.xml",
|
||||
],
|
||||
"demo": [
|
||||
"demo/pms_master_data.xml",
|
||||
|
||||
@@ -119,24 +119,28 @@
|
||||
<field name="room_type_id" ref="pms_room_type_0" />
|
||||
<field name="floor_id" ref="pms_floor_1" />
|
||||
<field name="capacity">2</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_1" model="pms.room">
|
||||
<field name="name">Single-101</field>
|
||||
<field name="room_type_id" ref="pms_room_type_1" />
|
||||
<field name="floor_id" ref="pms_floor_1" />
|
||||
<field name="capacity">1</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_2" model="pms.room">
|
||||
<field name="name">Single-102</field>
|
||||
<field name="room_type_id" ref="pms_room_type_1" />
|
||||
<field name="floor_id" ref="pms_floor_1" />
|
||||
<field name="capacity">1</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_3" model="pms.room">
|
||||
<field name="name">Single-103</field>
|
||||
<field name="room_type_id" ref="pms_room_type_1" />
|
||||
<field name="floor_id" ref="pms_floor_1" />
|
||||
<field name="capacity">1</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_4" model="pms.room">
|
||||
<field name="name">Double-201</field>
|
||||
@@ -144,24 +148,28 @@
|
||||
<field name="floor_id" ref="pms_floor_2" />
|
||||
<field name="capacity">2</field>
|
||||
<field name="extra_beds_allowed">1</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_5" model="pms.room">
|
||||
<field name="name">Double-202</field>
|
||||
<field name="room_type_id" ref="pms_room_type_2" />
|
||||
<field name="floor_id" ref="pms_floor_2" />
|
||||
<field name="capacity">2</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_6" model="pms.room">
|
||||
<field name="name">Triple-203</field>
|
||||
<field name="room_type_id" ref="pms_room_type_3" />
|
||||
<field name="floor_id" ref="pms_floor_2" />
|
||||
<field name="capacity">3</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<record id="pms_room_7" model="pms.room">
|
||||
<field name="name">Open Talk Away Room</field>
|
||||
<field name="room_type_id" ref="pms_room_type_4" />
|
||||
<field name="floor_id" ref="pms_floor_0" />
|
||||
<field name="capacity">1</field>
|
||||
<field name="pms_property_id" ref="pms.main_pms_property" />
|
||||
</record>
|
||||
<!-- product.product for pms services -->
|
||||
<record id="pms_service_0" model="product.product">
|
||||
|
||||
@@ -41,3 +41,7 @@ from . import pms_board_service_room_type_line
|
||||
from . import pms_board_service_line
|
||||
from . import account_move_line
|
||||
from . import pms_cancelation_rule
|
||||
from . import folio_sale_line
|
||||
from . import account_bank_statement_line
|
||||
from . import account_bank_statement
|
||||
from . import account_journal
|
||||
|
||||
7
pms/models/account_bank_statement.py
Normal file
7
pms/models/account_bank_statement.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountBankStatement(models.Model):
|
||||
_inherit = "account.bank.statement"
|
||||
|
||||
property_id = fields.Many2one("pms.property", string="Property", copy=False)
|
||||
29
pms/models/account_bank_statement_line.py
Normal file
29
pms/models/account_bank_statement_line.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = "account.bank.statement.line"
|
||||
|
||||
statement_folio_ids = fields.Many2many(
|
||||
"pms.folio", string="Folios", ondelete="cascade"
|
||||
)
|
||||
reservation_ids = fields.Many2many(
|
||||
"pms.reservation", string="Reservations", ondelete="cascade"
|
||||
)
|
||||
service_ids = fields.Many2many("pms.service", string="Services", ondelete="cascade")
|
||||
|
||||
@api.model
|
||||
def _prepare_move_line_default_vals(self, counterpart_account_id=None):
|
||||
line_vals_list = super(
|
||||
AccountBankStatementLine, self
|
||||
)._prepare_move_line_default_vals(counterpart_account_id)
|
||||
if self.statement_folio_ids:
|
||||
for line in line_vals_list:
|
||||
line.update(
|
||||
{
|
||||
"folio_ids": [(6, 0, self.statement_folio_ids.ids)],
|
||||
"reservation_ids": [(6, 0, self.reservation_ids.ids)],
|
||||
"service_ids": [(6, 0, self.service_ids.ids)],
|
||||
}
|
||||
)
|
||||
return line_vals_list
|
||||
21
pms/models/account_journal.py
Normal file
21
pms/models/account_journal.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
pms_property_ids = fields.Many2many("pms.property", string="Property", copy=False)
|
||||
|
||||
@api.constrains("pms_property_ids", "company_id")
|
||||
def _check_property_company_integrity(self):
|
||||
for rec in self:
|
||||
if rec.company_id and rec.pms_property_ids:
|
||||
property_companies = rec.pms_property_ids.mapped("company_id")
|
||||
if len(property_companies) > 1 or rec.company_id != property_companies:
|
||||
raise UserError(
|
||||
_(
|
||||
"The company of the properties must match "
|
||||
"the company on account journal"
|
||||
)
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# Copyright 2017 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
import json
|
||||
@@ -15,7 +14,7 @@ class AccountMove(models.Model):
|
||||
comodel_name="pms.folio", compute="_compute_folio_origin"
|
||||
)
|
||||
pms_property_id = fields.Many2one("pms.property")
|
||||
from_folio = fields.Boolean(compute="_compute_folio_origin")
|
||||
from_reservation = fields.Boolean(compute="_compute_from_reservation")
|
||||
outstanding_folios_debits_widget = fields.Text(
|
||||
compute="_compute_get_outstanding_folios_JSON"
|
||||
)
|
||||
@@ -27,14 +26,17 @@ class AccountMove(models.Model):
|
||||
|
||||
def _compute_folio_origin(self):
|
||||
for inv in self:
|
||||
inv.from_folio = False
|
||||
inv.folio_ids = False
|
||||
folios = inv.mapped("invoice_line_ids.reservation_ids.folio_id")
|
||||
folios |= inv.mapped("invoice_line_ids.service_ids.folio_id")
|
||||
folios = inv.mapped("invoice_line_ids.folio_ids")
|
||||
if folios:
|
||||
inv.from_folio = True
|
||||
inv.folio_ids = [(6, 0, folios.ids)]
|
||||
|
||||
def _compute_from_reservation(self):
|
||||
for inv in self:
|
||||
inv.from_reservation = False
|
||||
if len(inv.invoice_line_ids.mapped("reservation_line_ids")) > 0:
|
||||
inv.from_reservation = True
|
||||
|
||||
# Action methods
|
||||
|
||||
def action_folio_payments(self):
|
||||
@@ -55,6 +57,7 @@ class AccountMove(models.Model):
|
||||
}
|
||||
|
||||
# Business methods
|
||||
|
||||
def _compute_get_outstanding_folios_JSON(self):
|
||||
self.ensure_one()
|
||||
self.outstanding_folios_debits_widget = json.dumps(False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# Copyright 2017 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
@@ -35,3 +35,82 @@ class AccountMoveLine(models.Model):
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
folio_line_ids = fields.Many2many(
|
||||
"folio.sale.line",
|
||||
"folio_sale_line_invoice_rel",
|
||||
"invoice_line_id",
|
||||
"sale_line_id",
|
||||
string="Folio Lines",
|
||||
copy=False,
|
||||
)
|
||||
folio_ids = fields.Many2many(
|
||||
"pms.folio",
|
||||
"payment_folio_rel",
|
||||
"move_id",
|
||||
"folio_id",
|
||||
string="Folios",
|
||||
ondelete="cascade",
|
||||
compute="_compute_folio_ids",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
name = fields.Char(
|
||||
compute="_compute_name",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends("service_ids", "reservation_ids")
|
||||
def _compute_folio_ids(self):
|
||||
for record in self:
|
||||
if record.service_ids:
|
||||
record.folio_ids = record.mapped("service_ids.folio_id")
|
||||
elif record.reservation_ids:
|
||||
record.folio_ids = record.mapped("reservation_ids.folio_id")
|
||||
elif not record.folio_ids:
|
||||
record.folio_ids = False
|
||||
|
||||
def invoice_filter_days(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"pms.pms_invoice_filter_days_action"
|
||||
)
|
||||
# Force the values of the move line in the context to avoid issues
|
||||
ctx = dict(self.env.context)
|
||||
ctx.pop("active_id", None)
|
||||
ctx["active_ids"] = self.ids
|
||||
ctx["active_model"] = "account.move.line"
|
||||
action["context"] = ctx
|
||||
return action
|
||||
|
||||
def _copy_data_extend_business_fields(self, values):
|
||||
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
|
||||
values["folio_line_ids"] = [(6, None, self.folio_line_ids.ids)]
|
||||
values["reservation_line_ids"] = [(6, None, self.reservation_line_ids.ids)]
|
||||
values["service_ids"] = [(6, None, self.service_ids.ids)]
|
||||
values["reservation_ids"] = [(6, None, self.reservation_ids.ids)]
|
||||
|
||||
@api.depends("reservation_line_ids")
|
||||
def _compute_name(self):
|
||||
if hasattr(super(), "_compute_name"):
|
||||
super()._compute_field()
|
||||
for record in self:
|
||||
if record.reservation_line_ids:
|
||||
record.name = record._get_compute_name()
|
||||
|
||||
def _get_compute_name(self):
|
||||
self.ensure_one()
|
||||
if self.reservation_line_ids:
|
||||
month = False
|
||||
name = False
|
||||
lines = self.reservation_line_ids.sorted("date")
|
||||
for date in lines.mapped("date"):
|
||||
if date.month != month:
|
||||
name = name + "\n" if name else ""
|
||||
name += date.strftime("%B-%Y") + ": "
|
||||
name += date.strftime("%d")
|
||||
month = date.month
|
||||
else:
|
||||
name += ", " + date.strftime("%d")
|
||||
return name
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright 2017 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import except_orm
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
@@ -9,78 +8,6 @@ class AccountPayment(models.Model):
|
||||
|
||||
# Fields declaration
|
||||
folio_id = fields.Many2one("pms.folio", string="Folio Reference")
|
||||
amount_total_folio = fields.Float(
|
||||
compute="_compute_folio_amount",
|
||||
store=True,
|
||||
string="Total amount in folio",
|
||||
)
|
||||
save_amount = fields.Monetary(string="onchange_amount")
|
||||
save_date = fields.Date()
|
||||
save_journal_id = fields.Integer()
|
||||
|
||||
# Compute and Search methods
|
||||
|
||||
@api.depends("state")
|
||||
def _compute_folio_amount(self):
|
||||
# FIXME: Finalize method
|
||||
res = []
|
||||
fol = ()
|
||||
for payment in self:
|
||||
if payment.folio_id:
|
||||
fol = payment.env["pms.folio"].search(
|
||||
[("id", "=", payment.folio_id.id)]
|
||||
)
|
||||
else:
|
||||
return
|
||||
if not any(fol):
|
||||
return
|
||||
if len(fol) > 1:
|
||||
raise except_orm(
|
||||
_("Warning"),
|
||||
_(
|
||||
"This pay is related with \
|
||||
more than one Reservation."
|
||||
),
|
||||
)
|
||||
else:
|
||||
fol.compute_amount()
|
||||
return res
|
||||
|
||||
# Constraints and onchanges
|
||||
# @api.onchange("amount", "payment_date", "journal_id")
|
||||
# def onchange_amount(self):
|
||||
# if self._origin:
|
||||
# self.save_amount = self._origin.amount
|
||||
# self.save_journal_id = self._origin.journal_id.id
|
||||
# self.save_date = self._origin.payment_date
|
||||
|
||||
# Action methods
|
||||
# def return_payment_folio(self):
|
||||
# journal = self.journal_id
|
||||
# partner = self.partner_id
|
||||
# amount = self.amount
|
||||
# reference = self.communication
|
||||
# account_move_lines = self.move_line_ids.filtered(
|
||||
# lambda x: (x.account_id.internal_type == "receivable")
|
||||
# )
|
||||
# return_line_vals = {
|
||||
# "move_line_ids": [(6, False, [x.id for x in account_move_lines])],
|
||||
# "partner_id": partner.id,
|
||||
# "amount": amount,
|
||||
# "reference": reference,
|
||||
# }
|
||||
# return_vals = {
|
||||
# "journal_id": journal.id,
|
||||
# "line_ids": [(0, 0, return_line_vals)],
|
||||
# }
|
||||
# return_pay = self.env["payment.return"].create(return_vals)
|
||||
# if self.save_amount:
|
||||
# self.amount = self.save_amount
|
||||
# if self.save_date:
|
||||
# self.payment_date = self.save_date
|
||||
# if self.save_journal_id:
|
||||
# self.journal_id = self.env["account.journal"].browse(self.save_journal_id)
|
||||
# return_pay.action_confirm()
|
||||
|
||||
# Business methods
|
||||
|
||||
|
||||
857
pms/models/folio_sale_line.py
Normal file
857
pms/models/folio_sale_line.py
Normal file
@@ -0,0 +1,857 @@
|
||||
# Copyright 2020 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
|
||||
|
||||
class FolioSaleLine(models.Model):
|
||||
_name = "folio.sale.line"
|
||||
_description = "Folio Sale Line"
|
||||
_order = "folio_id, sequence, id"
|
||||
_check_company_auto = True
|
||||
|
||||
@api.depends("state", "product_uom_qty", "qty_to_invoice", "qty_invoiced")
|
||||
def _compute_invoice_status(self):
|
||||
"""
|
||||
Compute the invoice status of a SO line:
|
||||
Its if compute based on reservations/services associated status
|
||||
"""
|
||||
precision = self.env["decimal.precision"].precision_get(
|
||||
"Product Unit of Measure"
|
||||
)
|
||||
for line in self:
|
||||
if line.state == "draft":
|
||||
line.invoice_status = "no"
|
||||
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
|
||||
line.invoice_status = "to invoice"
|
||||
elif (
|
||||
float_compare(
|
||||
line.qty_invoiced,
|
||||
line.product_uom_qty,
|
||||
precision_digits=precision,
|
||||
)
|
||||
>= 0
|
||||
):
|
||||
line.invoice_status = "invoiced"
|
||||
else:
|
||||
line.invoice_status = "no"
|
||||
|
||||
@api.depends("reservation_line_ids", "service_id")
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
if not record.name_updated:
|
||||
record.name = record._get_compute_name()
|
||||
|
||||
@api.depends("name")
|
||||
def _compute_name_updated(self):
|
||||
self.name_updated = False
|
||||
for record in self.filtered("name"):
|
||||
if record.name != record._get_compute_name():
|
||||
record.name_updated = True
|
||||
|
||||
def _get_compute_name(self):
|
||||
self.ensure_one()
|
||||
if self.reservation_line_ids:
|
||||
month = False
|
||||
name = False
|
||||
lines = self.reservation_line_ids.sorted("date")
|
||||
for date in lines.mapped("date"):
|
||||
if date.month != month:
|
||||
name = name + "\n" if name else ""
|
||||
name += date.strftime("%B-%Y") + ": "
|
||||
name += date.strftime("%d")
|
||||
month = date.month
|
||||
else:
|
||||
name += ", " + date.strftime("%d")
|
||||
return name
|
||||
elif self.service_id:
|
||||
return self.service_id.name
|
||||
else:
|
||||
return False
|
||||
|
||||
@api.depends("service_id", "service_id.price_unit")
|
||||
def _compute_price_unit(self):
|
||||
"""
|
||||
Compute unit prices of services
|
||||
On reservations the unit price is compute by group in folio
|
||||
"""
|
||||
for record in self:
|
||||
if record.service_id:
|
||||
record.price_unit = record.service_id.price_unit
|
||||
elif not record.price_unit:
|
||||
record.price_unit = False
|
||||
|
||||
@api.depends("product_uom_qty", "discount", "price_unit", "tax_ids")
|
||||
def _compute_amount(self):
|
||||
"""
|
||||
Compute the amounts of the Sale line.
|
||||
"""
|
||||
for line in self:
|
||||
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
taxes = line.tax_ids.compute_all(
|
||||
price,
|
||||
line.folio_id.currency_id,
|
||||
line.product_uom_qty,
|
||||
product=line.product_id,
|
||||
)
|
||||
line.update(
|
||||
{
|
||||
"price_tax": sum(
|
||||
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
||||
),
|
||||
"price_total": taxes["total_included"],
|
||||
"price_subtotal": taxes["total_excluded"],
|
||||
}
|
||||
)
|
||||
if self.env.context.get(
|
||||
"import_file", False
|
||||
) and not self.env.user.user_has_groups("account.group_account_manager"):
|
||||
line.tax_ids.invalidate_cache(
|
||||
["invoice_repartition_line_ids"], [line.tax_ids.id]
|
||||
)
|
||||
|
||||
@api.depends("reservation_id.tax_ids", "service_id.tax_ids")
|
||||
def _compute_tax_ids(self):
|
||||
for record in self:
|
||||
record.tax_ids = (
|
||||
record.service_id.tax_ids
|
||||
if record.service_id
|
||||
else record.reservation_id.tax_ids
|
||||
)
|
||||
|
||||
@api.depends("service_id", "service_id.discount")
|
||||
def _compute_discount(self):
|
||||
self.discount = 0.0
|
||||
for record in self.filtered("service_id"):
|
||||
record.discount = record.service_id.discount
|
||||
|
||||
@api.depends("reservation_id.room_type_id", "service_id.product_id")
|
||||
def _compute_product_id(self):
|
||||
for record in self:
|
||||
if record.reservation_id:
|
||||
record.product_id = record.reservation_id.room_type_id.product_id
|
||||
elif record.service_id:
|
||||
record.product_id = record.service_id.product_id
|
||||
else:
|
||||
record.product_id = False
|
||||
|
||||
# @api.depends('product_id', 'folio_id.state', 'qty_invoiced', 'qty_delivered')
|
||||
# def _compute_product_updatable(self):
|
||||
# for line in self:
|
||||
# if line.state in ['done', 'cancel'] or (
|
||||
# line.state == 'sale' and (
|
||||
# line.qty_invoiced > 0 or line.qty_delivered > 0)):
|
||||
# line.product_updatable = False
|
||||
# else:
|
||||
# line.product_updatable = True
|
||||
|
||||
# no trigger product_id.invoice_policy to avoid retroactively changing SO
|
||||
@api.depends("qty_invoiced", "product_uom_qty", "folio_id.state")
|
||||
def _compute_get_to_invoice_qty(self):
|
||||
"""
|
||||
Compute the quantity to invoice.
|
||||
If the invoice policy is order, the quantity to invoice is
|
||||
calculated from the ordered quantity.
|
||||
Otherwise, the quantity delivered is used.
|
||||
"""
|
||||
for line in self:
|
||||
if line.folio_id.state not in ["draft"]:
|
||||
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
||||
else:
|
||||
line.qty_to_invoice = 0
|
||||
|
||||
@api.depends(
|
||||
"invoice_lines.move_id.state",
|
||||
"invoice_lines.quantity",
|
||||
"untaxed_amount_to_invoice",
|
||||
)
|
||||
def _compute_get_invoice_qty(self):
|
||||
"""
|
||||
Compute the quantity invoiced. If case of a refund,
|
||||
the quantity invoiced is decreased. Note
|
||||
that this is the case only if the refund is
|
||||
generated from the Folio and that is intentional: if
|
||||
a refund made would automatically decrease the invoiced quantity,
|
||||
then there is a risk of reinvoicing
|
||||
it automatically, which may not be wanted at all.
|
||||
That's why the refund has to be created from the Folio
|
||||
"""
|
||||
for line in self:
|
||||
qty_invoiced = 0.0
|
||||
for invoice_line in line.invoice_lines:
|
||||
if invoice_line.move_id.state != "cancel":
|
||||
if invoice_line.move_id.move_type == "out_invoice":
|
||||
qty_invoiced += invoice_line.product_uom_id._compute_quantity(
|
||||
invoice_line.quantity, line.product_uom
|
||||
)
|
||||
elif invoice_line.move_id.move_type == "out_refund":
|
||||
if (
|
||||
not line.is_downpayment
|
||||
or line.untaxed_amount_to_invoice == 0
|
||||
):
|
||||
qty_invoiced -= (
|
||||
invoice_line.product_uom_id._compute_quantity(
|
||||
invoice_line.quantity, line.product_uom
|
||||
)
|
||||
)
|
||||
line.qty_invoiced = qty_invoiced
|
||||
|
||||
@api.depends("price_unit", "discount")
|
||||
def _compute_get_price_reduce(self):
|
||||
for line in self:
|
||||
line.price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
|
||||
|
||||
@api.depends("price_total", "product_uom_qty")
|
||||
def _compute_get_price_reduce_tax(self):
|
||||
for line in self:
|
||||
line.price_reduce_taxinc = (
|
||||
line.price_total / line.product_uom_qty if line.product_uom_qty else 0.0
|
||||
)
|
||||
|
||||
@api.depends("price_subtotal", "product_uom_qty")
|
||||
def _compute_get_price_reduce_notax(self):
|
||||
for line in self:
|
||||
line.price_reduce_taxexcl = (
|
||||
line.price_subtotal / line.product_uom_qty
|
||||
if line.product_uom_qty
|
||||
else 0.0
|
||||
)
|
||||
|
||||
# @api.model
|
||||
# def _prepare_add_missing_fields(self, values):
|
||||
# """ Deduce missing required fields from the onchange """
|
||||
# res = {}
|
||||
# onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_ids']
|
||||
# if values.get('folio_id') and values.get('product_id') and any(
|
||||
# f not in values for f in onchange_fields
|
||||
# ):
|
||||
# line = self.new(values)
|
||||
# line.product_id_change()
|
||||
# for field in onchange_fields:
|
||||
# if field not in values:
|
||||
# res[field] = line._fields[field].convert_to_write(
|
||||
# line[field], line
|
||||
# )
|
||||
# return res
|
||||
|
||||
# @api.model_create_multi
|
||||
# def create(self, vals_list):
|
||||
# for values in vals_list:
|
||||
# if values.get('display_type', self.default_get(
|
||||
# ['display_type'])['display_type']
|
||||
# ):
|
||||
# values.update(product_id=False, price_unit=0,
|
||||
# product_uom_qty=0, product_uom=False,
|
||||
# customer_lead=0)
|
||||
|
||||
# values.update(self._prepare_add_missing_fields(values))
|
||||
|
||||
# lines = super().create(vals_list)
|
||||
# for line in lines:
|
||||
# if line.product_id and line.folio_id.state == 'sale':
|
||||
# msg = _("Extra line with %s ") % (line.product_id.display_name,)
|
||||
# line.folio_id.message_post(body=msg)
|
||||
# # create an analytic account if at least an expense product
|
||||
# if line.product_id.expense_policy not in [False, 'no'] and \
|
||||
# not line.folio_id.analytic_account_id:
|
||||
# line.folio_id._create_analytic_account()
|
||||
# return lines
|
||||
|
||||
# _sql_constraints = [
|
||||
# ('accountable_required_fields',
|
||||
# "CHECK(display_type IS NOT NULL OR \
|
||||
# (product_id IS NOT NULL AND product_uom IS NOT NULL))",
|
||||
# "Missing required fields on accountable sale order line."),
|
||||
# ('non_accountable_null_fields',
|
||||
# "CHECK(display_type IS NULL OR (product_id IS NULL AND \
|
||||
# price_unit = 0 AND product_uom_qty = 0 AND \
|
||||
# product_uom IS NULL AND customer_lead = 0))",
|
||||
# "Forbidden values on non-accountable sale order line"),
|
||||
# ]
|
||||
|
||||
def _update_line_quantity(self, values):
|
||||
folios = self.mapped("folio_id")
|
||||
for order in folios:
|
||||
order_lines = self.filtered(lambda x: x.folio_id == order)
|
||||
msg = "<b>" + _("The ordered quantity has been updated.") + "</b><ul>"
|
||||
for line in order_lines:
|
||||
msg += "<li> %s: <br/>" % line.product_id.display_name
|
||||
msg += (
|
||||
_(
|
||||
"Ordered Quantity: %(old_qty)s -> %(new_qty)s",
|
||||
old_qty=line.product_uom_qty,
|
||||
new_qty=values["product_uom_qty"],
|
||||
)
|
||||
+ "<br/>"
|
||||
)
|
||||
# if line.product_id.type in ('consu', 'product'):
|
||||
# msg += _("Delivered Quantity: %s", line.qty_delivered) + "<br/>"
|
||||
msg += _("Invoiced Quantity: %s", line.qty_invoiced) + "<br/>"
|
||||
msg += "</ul>"
|
||||
order.message_post(body=msg)
|
||||
|
||||
# def write(self, values):
|
||||
# if 'display_type' in values and self.filtered(
|
||||
# lambda line: line.display_type != values.get('display_type')):
|
||||
# raise UserError(_("You cannot change the type of a sale order line.\
|
||||
# Instead you should delete the current line and create \
|
||||
# a new line of the proper type."))
|
||||
|
||||
# if 'product_uom_qty' in values:
|
||||
# precision = self.env['decimal.precision'].precision_get(
|
||||
# 'Product Unit of Measure'
|
||||
# )
|
||||
# self.filtered(
|
||||
# lambda r: r.state == 'sale' and \
|
||||
# float_compare(
|
||||
# r.product_uom_qty,
|
||||
# values['product_uom_qty'],
|
||||
# precision_digits=precision) != 0)._update_line_quantity(
|
||||
# values
|
||||
# )
|
||||
|
||||
# # Prevent writing on a locked SO.
|
||||
# protected_fields = self._get_protected_fields()
|
||||
# if 'done' in self.mapped('folio_id.state') and any(
|
||||
# f in values.keys() for f in protected_fields
|
||||
# ):
|
||||
# protected_fields_modified = list(set(protected_fields) & set(
|
||||
# values.keys()
|
||||
# ))
|
||||
# fields = self.env['ir.model.fields'].search([
|
||||
# ('name', 'in', protected_fields_modified),
|
||||
# ('model', '=', self._name)
|
||||
# ])
|
||||
# raise UserError(
|
||||
# _('It is forbidden to modify the following \
|
||||
# fields in a locked order:\n%s')
|
||||
# % '\n'.join(fields.mapped('field_description'))
|
||||
# )
|
||||
|
||||
# result = super(SaleOrderLine, self).write(values)
|
||||
# return result
|
||||
|
||||
folio_id = fields.Many2one(
|
||||
"pms.folio",
|
||||
string="Folio Reference",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
copy=False,
|
||||
)
|
||||
reservation_id = fields.Many2one(
|
||||
"pms.reservation",
|
||||
string="Reservation Reference",
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
copy=False,
|
||||
)
|
||||
service_id = fields.Many2one(
|
||||
"pms.service",
|
||||
string="Service Reference",
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
copy=False,
|
||||
)
|
||||
name = fields.Text(
|
||||
string="Description", compute="_compute_name", store=True, readonly=False
|
||||
)
|
||||
name_updated = fields.Boolean(compute="_compute_name_updated", store=True)
|
||||
reservation_line_ids = fields.Many2many(
|
||||
"pms.reservation.line",
|
||||
string="Nights",
|
||||
)
|
||||
sequence = fields.Integer(string="Sequence", default=10)
|
||||
|
||||
invoice_lines = fields.Many2many(
|
||||
"account.move.line",
|
||||
"folio_sale_line_invoice_rel",
|
||||
"sale_line_id",
|
||||
"invoice_line_id",
|
||||
string="Invoice Lines",
|
||||
copy=False,
|
||||
)
|
||||
invoice_status = fields.Selection(
|
||||
[
|
||||
("upselling", "Upselling Opportunity"),
|
||||
("invoiced", "Fully Invoiced"),
|
||||
("to invoice", "To Invoice"),
|
||||
("no", "Nothing to Invoice"),
|
||||
],
|
||||
string="Invoice Status",
|
||||
compute="_compute_invoice_status",
|
||||
store=True,
|
||||
readonly=True,
|
||||
default="no",
|
||||
)
|
||||
price_unit = fields.Float(
|
||||
"Unit Price",
|
||||
digits="Product Price",
|
||||
compute="_compute_price_unit",
|
||||
store=True,
|
||||
)
|
||||
|
||||
price_subtotal = fields.Monetary(
|
||||
compute="_compute_amount", string="Subtotal", readonly=True, store=True
|
||||
)
|
||||
price_tax = fields.Float(
|
||||
compute="_compute_amount", string="Total Tax", readonly=True, store=True
|
||||
)
|
||||
price_total = fields.Monetary(
|
||||
compute="_compute_amount", string="Total", readonly=True, store=True
|
||||
)
|
||||
price_reduce = fields.Float(
|
||||
compute="_compute_get_price_reduce",
|
||||
string="Price Reduce",
|
||||
digits="Product Price",
|
||||
readonly=True,
|
||||
store=True,
|
||||
)
|
||||
tax_ids = fields.Many2many(
|
||||
"account.tax",
|
||||
compute="_compute_tax_ids",
|
||||
store=True,
|
||||
string="Taxes",
|
||||
domain=["|", ("active", "=", False), ("active", "=", True)],
|
||||
)
|
||||
price_reduce_taxinc = fields.Monetary(
|
||||
compute="_compute_get_price_reduce_tax",
|
||||
string="Price Reduce Tax inc",
|
||||
readonly=True,
|
||||
store=True,
|
||||
)
|
||||
price_reduce_taxexcl = fields.Monetary(
|
||||
compute="_compute_get_price_reduce_notax",
|
||||
string="Price Reduce Tax excl",
|
||||
readonly=True,
|
||||
store=True,
|
||||
)
|
||||
|
||||
discount = fields.Float(
|
||||
string="Discount (%)",
|
||||
digits="Discount",
|
||||
compute="_compute_discount",
|
||||
store=True,
|
||||
)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
"product.product",
|
||||
string="Product",
|
||||
domain="[('sale_ok', '=', True),\
|
||||
'|', ('company_id', '=', False), \
|
||||
('company_id', '=', company_id)]",
|
||||
change_default=True,
|
||||
ondelete="restrict",
|
||||
check_company=True,
|
||||
compute="_compute_product_id",
|
||||
store=True,
|
||||
)
|
||||
# product_updatable = fields.Boolean(
|
||||
# compute='_compute_product_updatable',
|
||||
# string='Can Edit Product',
|
||||
# readonly=True,
|
||||
# default=True)
|
||||
product_uom_qty = fields.Float(
|
||||
string="Quantity",
|
||||
digits="Product Unit of Measure",
|
||||
compute="_compute_product_uom_qty",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
product_uom = fields.Many2one(
|
||||
"uom.uom",
|
||||
string="Unit of Measure",
|
||||
domain="[('category_id', '=', product_uom_category_id)]",
|
||||
)
|
||||
product_uom_category_id = fields.Many2one(
|
||||
related="product_id.uom_id.category_id", readonly=True
|
||||
)
|
||||
product_uom_readonly = fields.Boolean(compute="_compute_product_uom_readonly")
|
||||
product_custom_attribute_value_ids = fields.One2many(
|
||||
"product.attribute.custom.value",
|
||||
"sale_order_line_id",
|
||||
string="Custom Values",
|
||||
copy=True,
|
||||
)
|
||||
|
||||
qty_to_invoice = fields.Float(
|
||||
compute="_compute_get_to_invoice_qty",
|
||||
string="To Invoice Quantity",
|
||||
store=True,
|
||||
readonly=True,
|
||||
digits="Product Unit of Measure",
|
||||
)
|
||||
qty_invoiced = fields.Float(
|
||||
compute="_compute_get_invoice_qty",
|
||||
string="Invoiced Quantity",
|
||||
store=True,
|
||||
readonly=True,
|
||||
compute_sudo=True,
|
||||
digits="Product Unit of Measure",
|
||||
)
|
||||
|
||||
untaxed_amount_invoiced = fields.Monetary(
|
||||
"Untaxed Invoiced Amount",
|
||||
compute="_compute_untaxed_amount_invoiced",
|
||||
compute_sudo=True,
|
||||
store=True,
|
||||
)
|
||||
untaxed_amount_to_invoice = fields.Monetary(
|
||||
"Untaxed Amount To Invoice",
|
||||
compute="_compute_untaxed_amount_to_invoice",
|
||||
compute_sudo=True,
|
||||
store=True,
|
||||
)
|
||||
|
||||
currency_id = fields.Many2one(
|
||||
related="folio_id.currency_id",
|
||||
depends=["folio_id.currency_id"],
|
||||
store=True,
|
||||
string="Currency",
|
||||
readonly=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related="folio_id.company_id",
|
||||
string="Company",
|
||||
store=True,
|
||||
readonly=True,
|
||||
index=True,
|
||||
)
|
||||
folio_partner_id = fields.Many2one(
|
||||
related="folio_id.partner_id", store=True, string="Customer", readonly=False
|
||||
)
|
||||
analytic_tag_ids = fields.Many2many(
|
||||
"account.analytic.tag",
|
||||
string="Analytic Tags",
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
)
|
||||
analytic_line_ids = fields.One2many(
|
||||
"account.analytic.line", "so_line", string="Analytic lines"
|
||||
)
|
||||
is_downpayment = fields.Boolean(
|
||||
string="Is a down payment",
|
||||
help="Down payments are made when creating invoices from a folio."
|
||||
" They are not copied when duplicating a folio.",
|
||||
)
|
||||
|
||||
state = fields.Selection(
|
||||
related="folio_id.state",
|
||||
string="Folio Status",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
store=True,
|
||||
)
|
||||
|
||||
display_type = fields.Selection(
|
||||
[("line_section", "Section"), ("line_note", "Note")],
|
||||
default=False,
|
||||
help="Technical field for UX purpose.",
|
||||
)
|
||||
|
||||
@api.depends("reservation_line_ids", "service_id")
|
||||
def _compute_product_uom_qty(self):
|
||||
for line in self:
|
||||
if line.reservation_line_ids:
|
||||
line.product_uom_qty = len(line.reservation_line_ids)
|
||||
elif line.service_id:
|
||||
line.product_uom_qty = line.service_id.product_qty
|
||||
elif not line.product_uom_qty:
|
||||
line.product_uom_qty = False
|
||||
|
||||
@api.depends("state")
|
||||
def _compute_product_uom_readonly(self):
|
||||
for line in self:
|
||||
line.product_uom_readonly = line.state in ["sale", "done", "cancel"]
|
||||
|
||||
@api.depends(
|
||||
"invoice_lines",
|
||||
"invoice_lines.price_total",
|
||||
"invoice_lines.move_id.state",
|
||||
"invoice_lines.move_id.move_type",
|
||||
)
|
||||
def _compute_untaxed_amount_invoiced(self):
|
||||
"""Compute the untaxed amount already invoiced from
|
||||
the sale order line, taking the refund attached
|
||||
the so line into account. This amount is computed as
|
||||
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
|
||||
where
|
||||
`inv_line` is a customer invoice line linked to the SO line
|
||||
`ref_line` is a customer credit note (refund) line linked to the SO line
|
||||
"""
|
||||
for line in self:
|
||||
amount_invoiced = 0.0
|
||||
for invoice_line in line.invoice_lines:
|
||||
if invoice_line.move_id.state == "posted":
|
||||
invoice_date = (
|
||||
invoice_line.move_id.invoice_date or fields.Date.today()
|
||||
)
|
||||
if invoice_line.move_id.move_type == "out_invoice":
|
||||
amount_invoiced += invoice_line.currency_id._convert(
|
||||
invoice_line.price_subtotal,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
invoice_date,
|
||||
)
|
||||
elif invoice_line.move_id.move_type == "out_refund":
|
||||
amount_invoiced -= invoice_line.currency_id._convert(
|
||||
invoice_line.price_subtotal,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
invoice_date,
|
||||
)
|
||||
line.untaxed_amount_invoiced = amount_invoiced
|
||||
|
||||
@api.depends(
|
||||
"state",
|
||||
"price_reduce",
|
||||
"product_id",
|
||||
"untaxed_amount_invoiced",
|
||||
"product_uom_qty",
|
||||
)
|
||||
def _compute_untaxed_amount_to_invoice(self):
|
||||
"""Total of remaining amount to invoice on the sale order line (taxes excl.) as
|
||||
total_sol - amount already invoiced
|
||||
where Total_sol depends on the invoice policy of the product.
|
||||
|
||||
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
|
||||
come only from the SO lines.
|
||||
"""
|
||||
for line in self:
|
||||
amount_to_invoice = 0.0
|
||||
if line.state != "draft":
|
||||
# Note: do not use price_subtotal field as it returns
|
||||
# zero when the ordered quantity is zero.
|
||||
# It causes problem for expense line (e.i.: ordered qty = 0,
|
||||
# deli qty = 4, price_unit = 20 ; subtotal is zero),
|
||||
# but when you can invoice the line,
|
||||
# you see an amount and not zero.
|
||||
# Since we compute untaxed amount, we can use directly the price
|
||||
# reduce (to include discount) without using `compute_all()`
|
||||
# method on taxes.
|
||||
price_subtotal = 0.0
|
||||
price_subtotal = line.price_reduce * line.product_uom_qty
|
||||
if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0:
|
||||
# As included taxes are not excluded from the computed subtotal,
|
||||
# `compute_all()` method has to be called to retrieve
|
||||
# the subtotal without them.
|
||||
# `price_reduce_taxexcl` cannot be used as it is computed from
|
||||
# `price_subtotal` field. (see upper Note)
|
||||
price_subtotal = line.tax_ids.compute_all(
|
||||
price_subtotal,
|
||||
currency=line.folio_id.currency_id,
|
||||
quantity=line.product_uom_qty,
|
||||
product=line.product_id,
|
||||
partner=line.folio_id.partner_shipping_id,
|
||||
)["total_excluded"]
|
||||
|
||||
if any(
|
||||
line.invoice_lines.mapped(lambda l: l.discount != line.discount)
|
||||
):
|
||||
# In case of re-invoicing with different
|
||||
# discount we try to calculate manually the
|
||||
# remaining amount to invoice
|
||||
amount = 0
|
||||
for inv_line in line.invoice_lines:
|
||||
if (
|
||||
len(
|
||||
inv_line.tax_ids.filtered(lambda tax: tax.price_include)
|
||||
)
|
||||
> 0
|
||||
):
|
||||
amount += inv_line.tax_ids.compute_all(
|
||||
inv_line.currency_id._convert(
|
||||
inv_line.price_unit,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
inv_line.date or fields.Date.today(),
|
||||
round=False,
|
||||
)
|
||||
* inv_line.quantity
|
||||
)["total_excluded"]
|
||||
else:
|
||||
amount += (
|
||||
inv_line.currency_id._convert(
|
||||
inv_line.price_unit,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
inv_line.date or fields.Date.today(),
|
||||
round=False,
|
||||
)
|
||||
* inv_line.quantity
|
||||
)
|
||||
|
||||
amount_to_invoice = max(price_subtotal - amount, 0)
|
||||
else:
|
||||
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
|
||||
|
||||
line.untaxed_amount_to_invoice = amount_to_invoice
|
||||
|
||||
def _get_invoice_line_sequence(self, new=0, old=0):
|
||||
"""
|
||||
Method intended to be overridden in third-party
|
||||
module if we want to prevent the resequencing of invoice lines.
|
||||
|
||||
:param int new: the new line sequence
|
||||
:param int old: the old line sequence
|
||||
|
||||
:return: the sequence of the SO line, by default the new one.
|
||||
"""
|
||||
return new or old
|
||||
|
||||
def _prepare_invoice_line(self, **optional_values):
|
||||
"""
|
||||
Prepare the dict of values to create the new invoice line for a folio sale line.
|
||||
|
||||
:param qty: float quantity to invoice
|
||||
:param optional_values: any parameter that
|
||||
should be added to the returned invoice line
|
||||
"""
|
||||
self.ensure_one()
|
||||
reservation = self.reservation_id
|
||||
service = self.service_id
|
||||
reservation_lines = self.reservation_line_ids.filtered(lambda l: not l.invoiced)
|
||||
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,
|
||||
"discount": self.discount,
|
||||
"price_unit": self.price_unit,
|
||||
"tax_ids": [(6, 0, self.tax_ids.ids)],
|
||||
"analytic_account_id": self.folio_id.analytic_account_id.id,
|
||||
"analytic_tag_ids": [(6, 0, self.analytic_tag_ids.ids)],
|
||||
"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)],
|
||||
}
|
||||
if optional_values:
|
||||
res.update(optional_values)
|
||||
if self.display_type:
|
||||
res["account_id"] = False
|
||||
return res
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for so_line in self.sudo():
|
||||
name = "{} - {}".format(
|
||||
so_line.folio_id.name,
|
||||
so_line.name and so_line.name.split("\n")[0] or so_line.product_id.name,
|
||||
)
|
||||
result.append((so_line.id, name))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _name_search(
|
||||
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
|
||||
):
|
||||
if operator in ("ilike", "like", "=", "=like", "=ilike"):
|
||||
args = expression.AND(
|
||||
[
|
||||
args or [],
|
||||
["|", ("folio_id.name", operator, name), ("name", operator, name)],
|
||||
]
|
||||
)
|
||||
return super(FolioSaleLine, self)._name_search(
|
||||
name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid
|
||||
)
|
||||
|
||||
def _check_line_unlink(self):
|
||||
"""
|
||||
Check wether a line can be deleted or not.
|
||||
|
||||
Lines cannot be deleted if the folio is confirmed; downpayment
|
||||
lines who have not yet been invoiced bypass that exception.
|
||||
:rtype: recordset folio.sale.line
|
||||
:returns: set of lines that cannot be deleted
|
||||
"""
|
||||
return self.filtered(
|
||||
lambda line: line.state not in ("draft")
|
||||
and (line.invoice_lines or not line.is_downpayment)
|
||||
)
|
||||
|
||||
# def unlink(self):
|
||||
# if self._check_line_unlink():
|
||||
# raise UserError(
|
||||
# _("""You can not remove an sale line once the sales
|
||||
# folio is confirmed.\n
|
||||
# You should rather set the quantity to 0.""")
|
||||
# )
|
||||
# return super(FolioSaleLine, self).unlink()
|
||||
|
||||
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
|
||||
"""Retrieve the price before applying the pricelist
|
||||
:param obj product: object of current product record
|
||||
:parem float qty: total quentity of product
|
||||
:param tuple price_and_rule: tuple(price, suitable_rule)
|
||||
coming from pricelist computation
|
||||
:param obj uom: unit of measure of current folio line
|
||||
:param integer pricelist_id: pricelist id of folio"""
|
||||
PricelistItem = self.env["product.pricelist.item"]
|
||||
field_name = "lst_price"
|
||||
currency_id = None
|
||||
product_currency = product.currency_id
|
||||
if rule_id:
|
||||
pricelist_item = PricelistItem.browse(rule_id)
|
||||
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
||||
while (
|
||||
pricelist_item.base == "pricelist"
|
||||
and pricelist_item.base_pricelist_id
|
||||
and pricelist_item.base_pricelist_id.discount_policy
|
||||
== "without_discount"
|
||||
):
|
||||
price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
||||
uom=uom.id
|
||||
).get_product_price_rule(product, qty, self.folio_id.partner_id)
|
||||
pricelist_item = PricelistItem.browse(rule_id)
|
||||
|
||||
if pricelist_item.base == "standard_price":
|
||||
field_name = "standard_price"
|
||||
product_currency = product.cost_currency_id
|
||||
elif (
|
||||
pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id
|
||||
):
|
||||
field_name = "price"
|
||||
product = product.with_context(
|
||||
pricelist=pricelist_item.base_pricelist_id.id
|
||||
)
|
||||
product_currency = pricelist_item.base_pricelist_id.currency_id
|
||||
currency_id = pricelist_item.pricelist_id.currency_id
|
||||
|
||||
if not currency_id:
|
||||
currency_id = product_currency
|
||||
cur_factor = 1.0
|
||||
else:
|
||||
if currency_id.id == product_currency.id:
|
||||
cur_factor = 1.0
|
||||
else:
|
||||
cur_factor = currency_id._get_conversion_rate(
|
||||
product_currency,
|
||||
currency_id,
|
||||
self.company_id or self.env.company,
|
||||
self.folio_id.date_order or fields.Date.today(),
|
||||
)
|
||||
|
||||
product_uom = self.env.context.get("uom") or product.uom_id.id
|
||||
if uom and uom.id != product_uom:
|
||||
# the unit price is in a different uom
|
||||
uom_factor = uom._compute_price(1.0, product.uom_id)
|
||||
else:
|
||||
uom_factor = 1.0
|
||||
|
||||
return product[field_name] * uom_factor * cur_factor, currency_id
|
||||
|
||||
def _get_protected_fields(self):
|
||||
return [
|
||||
"product_id",
|
||||
"name",
|
||||
"price_unit",
|
||||
"product_uom",
|
||||
"product_uom_qty",
|
||||
"tax_ids",
|
||||
"analytic_tag_ids",
|
||||
]
|
||||
@@ -106,7 +106,9 @@ class PmsBoardServiceRoomType(models.Model):
|
||||
# Action methods
|
||||
|
||||
def open_board_lines_form(self):
|
||||
action = self.env.ref("pms.action_pms_board_service_room_type_view").read()[0]
|
||||
action = (
|
||||
self.env.ref("pms.action_pms_board_service_room_type_view").sudo().read()[0]
|
||||
)
|
||||
action["views"] = [
|
||||
(self.env.ref("pms.pms_board_service_room_type_form").id, "form")
|
||||
]
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
from itertools import groupby
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,24 +28,21 @@ class PmsFolio(models.Model):
|
||||
result.append((folio.id, name))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _default_diff_invoicing(self):
|
||||
"""
|
||||
If the guest has an invoicing address set,
|
||||
this method return diff_invoicing = True, else, return False
|
||||
"""
|
||||
if "folio_id" in self.env.context:
|
||||
folio = self.env["pms.folio"].browse([self.env.context["folio_id"]])
|
||||
if folio.partner_id.id == folio.partner_invoice_id.id:
|
||||
return False
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _get_default_pms_property(self):
|
||||
return (
|
||||
self.env.user.pms_property_id
|
||||
) # TODO: Change by property env variable (like company)
|
||||
|
||||
def _default_note(self):
|
||||
return (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("account.use_invoice_terms")
|
||||
and self.env.company.invoice_terms
|
||||
or ""
|
||||
)
|
||||
|
||||
# Fields declaration
|
||||
name = fields.Char(
|
||||
string="Folio Number", readonly=True, index=True, default=lambda self: _("New")
|
||||
@@ -78,12 +78,30 @@ class PmsFolio(models.Model):
|
||||
help="Services detail provide to customer and it will "
|
||||
"include in main Invoice.",
|
||||
)
|
||||
sale_line_ids = fields.One2many(
|
||||
"folio.sale.line",
|
||||
"folio_id",
|
||||
compute="_compute_sale_line_ids",
|
||||
compute_sudo=True,
|
||||
store="True",
|
||||
)
|
||||
invoice_count = fields.Integer(
|
||||
string="Invoice Count", compute="_compute_get_invoiced", readonly=True
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
"Company",
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
move_line_ids = fields.Many2many(
|
||||
"account.move.line",
|
||||
"payment_folio_rel",
|
||||
"folio_id",
|
||||
"move_id",
|
||||
string="Payments",
|
||||
readonly=True,
|
||||
)
|
||||
analytic_account_id = fields.Many2one(
|
||||
"account.analytic.account",
|
||||
"Analytic Account",
|
||||
@@ -138,8 +156,15 @@ class PmsFolio(models.Model):
|
||||
ondelete="restrict",
|
||||
domain=[("channel_type", "=", "direct")],
|
||||
)
|
||||
payment_ids = fields.One2many("account.payment", "folio_id", readonly=True)
|
||||
# return_ids = fields.One2many("payment.return", "folio_id", readonly=True)
|
||||
transaction_ids = fields.Many2many(
|
||||
"payment.transaction",
|
||||
"folio_transaction_rel",
|
||||
"folio_id",
|
||||
"transaction_id",
|
||||
string="Transactions",
|
||||
copy=False,
|
||||
readonly=True,
|
||||
)
|
||||
payment_term_id = fields.Many2one(
|
||||
"account.payment.term",
|
||||
string="Payment Terms",
|
||||
@@ -172,9 +197,22 @@ class PmsFolio(models.Model):
|
||||
"account.move",
|
||||
string="Invoices",
|
||||
compute="_compute_get_invoiced",
|
||||
search="_search_invoice_ids",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
compute_sudo=True,
|
||||
)
|
||||
payment_state = fields.Selection(
|
||||
selection=[
|
||||
("not_paid", "Not Paid"),
|
||||
("paid", "Paid"),
|
||||
("partial", "Partially Paid"),
|
||||
],
|
||||
string="Payment Status",
|
||||
store=True,
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
compute="_compute_amount",
|
||||
)
|
||||
partner_invoice_id = fields.Many2one(
|
||||
"res.partner",
|
||||
@@ -289,7 +327,7 @@ class PmsFolio(models.Model):
|
||||
("no", "Nothing to Invoice"),
|
||||
],
|
||||
string="Invoice Status",
|
||||
compute="_compute_get_invoiced",
|
||||
compute="_compute_get_invoice_status",
|
||||
store=True,
|
||||
readonly=True,
|
||||
default="no",
|
||||
@@ -304,6 +342,12 @@ class PmsFolio(models.Model):
|
||||
advance has not been recorded",
|
||||
)
|
||||
sequence = fields.Integer(string="Sequence", default=10)
|
||||
note = fields.Text("Terms and conditions", default=_default_note)
|
||||
reference = fields.Char(
|
||||
string="Payment Ref.",
|
||||
copy=False,
|
||||
help="The payment communication of this sale order.",
|
||||
)
|
||||
|
||||
# Compute and Search methods
|
||||
@api.depends("reservation_ids", "reservation_ids.state")
|
||||
@@ -313,6 +357,100 @@ class PmsFolio(models.Model):
|
||||
folio.reservation_ids.filtered(lambda a: a.state != "cancelled")
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"reservation_ids",
|
||||
"service_ids",
|
||||
"service_ids.reservation_id",
|
||||
"reservation_ids.reservation_line_ids",
|
||||
"reservation_ids.reservation_line_ids.price",
|
||||
"reservation_ids.reservation_line_ids.discount",
|
||||
"reservation_ids.reservation_line_ids.cancel_discount",
|
||||
)
|
||||
def _compute_sale_line_ids(self):
|
||||
for folio in self:
|
||||
sale_lines = [(5, 0, 0)]
|
||||
reservations = folio.reservation_ids
|
||||
services_without_room = folio.service_ids.filtered(
|
||||
lambda s: not s.reservation_id
|
||||
)
|
||||
# TODO: Not delete old sale line ids
|
||||
for reservation in reservations:
|
||||
sale_lines.append(
|
||||
(
|
||||
0,
|
||||
False,
|
||||
{
|
||||
"display_type": "line_section",
|
||||
"name": reservation.name,
|
||||
},
|
||||
)
|
||||
)
|
||||
group_lines = {}
|
||||
for line in reservation.reservation_line_ids:
|
||||
# On resevations the price, and discounts fields are used
|
||||
# by group, we need pass this in the create line
|
||||
group_key = (
|
||||
reservation.id,
|
||||
line.price,
|
||||
line.discount,
|
||||
line.cancel_discount,
|
||||
)
|
||||
if line.cancel_discount == 100:
|
||||
continue
|
||||
discount_factor = 1.0
|
||||
for discount in [line.discount, line.cancel_discount]:
|
||||
discount_factor = discount_factor * ((100.0 - discount) / 100.0)
|
||||
final_discount = 100.0 - (discount_factor * 100.0)
|
||||
if group_key not in group_lines:
|
||||
group_lines[group_key] = {
|
||||
"reservation_id": reservation.id,
|
||||
"discount": final_discount,
|
||||
"price_unit": line.price,
|
||||
"reservation_line_ids": [(4, line.id)],
|
||||
}
|
||||
else:
|
||||
group_lines[group_key][("reservation_line_ids")].append(
|
||||
(4, line.id)
|
||||
)
|
||||
for item in group_lines.items():
|
||||
sale_lines.append((0, False, item[1]))
|
||||
for service in reservation.service_ids:
|
||||
# On service the price, and discounts fields are
|
||||
# compute in the sale.order.line
|
||||
sale_lines.append(
|
||||
(
|
||||
0,
|
||||
False,
|
||||
{
|
||||
"name": service.name,
|
||||
"service_id": service.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
if services_without_room:
|
||||
sale_lines.append(
|
||||
(
|
||||
0,
|
||||
False,
|
||||
{
|
||||
"display_type": "line_section",
|
||||
"name": _("Others"),
|
||||
},
|
||||
)
|
||||
)
|
||||
for service in services_without_room:
|
||||
sale_lines.append(
|
||||
(
|
||||
0,
|
||||
False,
|
||||
{
|
||||
"name": service.name,
|
||||
"service_id": service.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
folio.sale_line_ids = sale_lines
|
||||
|
||||
@api.depends("partner_id", "agency_id")
|
||||
def _compute_pricelist_id(self):
|
||||
for folio in self:
|
||||
@@ -365,100 +503,98 @@ class PmsFolio(models.Model):
|
||||
else:
|
||||
folio.commission = 0
|
||||
|
||||
@api.depends(
|
||||
"state", "reservation_ids.invoice_status", "service_ids.invoice_status"
|
||||
)
|
||||
@api.depends("sale_line_ids.invoice_lines")
|
||||
def _compute_get_invoiced(self):
|
||||
# The invoice_ids are obtained thanks to the invoice lines of the SO
|
||||
# lines, and we also search for possible refunds created directly from
|
||||
# existing invoices. This is necessary since such a refund is not
|
||||
# directly linked to the SO.
|
||||
for order in self:
|
||||
invoices = order.sale_line_ids.invoice_lines.move_id.filtered(
|
||||
lambda r: r.move_type in ("out_invoice", "out_refund")
|
||||
)
|
||||
order.move_ids = invoices
|
||||
order.invoice_count = len(invoices)
|
||||
|
||||
def _search_invoice_ids(self, operator, value):
|
||||
if operator == "in" and value:
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT array_agg(so.id)
|
||||
FROM pms_folio so
|
||||
JOIN folio_sale_line sol ON sol.folio_id = so.id
|
||||
JOIN folio_sale_line_invoice_rel soli_rel ON \
|
||||
soli_rel.sale_line_ids = sol.id
|
||||
JOIN account_move_line aml ON aml.id = soli_rel.invoice_line_id
|
||||
JOIN account_move am ON am.id = aml.move_id
|
||||
WHERE
|
||||
am.move_type in ('out_invoice', 'out_refund') AND
|
||||
am.id = ANY(%s)
|
||||
""",
|
||||
(list(value),),
|
||||
)
|
||||
so_ids = self.env.cr.fetchone()[0] or []
|
||||
return [("id", "in", so_ids)]
|
||||
return [
|
||||
"&",
|
||||
(
|
||||
"sale_line_ids.invoice_lines.move_id.move_type",
|
||||
"in",
|
||||
("out_invoice", "out_refund"),
|
||||
),
|
||||
("sale_line_ids.invoice_lines.move_id", operator, value),
|
||||
]
|
||||
|
||||
@api.depends("state", "sale_line_ids.invoice_status")
|
||||
def _compute_get_invoice_status(self):
|
||||
"""
|
||||
Compute the invoice status of a Folio. Possible statuses:
|
||||
- no: if the Folio is not in status 'sale' or 'done', we
|
||||
consider that there is nothing to invoice.
|
||||
This is also the default value if the conditions of no other
|
||||
status is met.
|
||||
- to invoice: if any Folio line is 'to invoice',
|
||||
the whole Folio is 'to invoice'
|
||||
- invoiced: if all Folio lines are invoiced, the Folio is invoiced.
|
||||
|
||||
The invoice_ids are obtained thanks to the invoice lines of the
|
||||
Folio lines, and we also search for possible refunds created
|
||||
directly from existing invoices. This is necessary since such a
|
||||
refund is not directly linked to the Folio.
|
||||
- no: if the Folio is in status 'draft', we consider that there is nothing to
|
||||
invoice. This is also the default value if the conditions of no
|
||||
other status is met.
|
||||
- to invoice: if any SO line is 'to invoice', the whole SO is 'to invoice'
|
||||
- invoiced: if all SO lines are invoiced, the SO is invoiced.
|
||||
- upselling: if all SO lines are invoiced or upselling, the status is upselling.
|
||||
"""
|
||||
self.move_ids = False
|
||||
for folio in self.filtered("pricelist_id"):
|
||||
move_ids = (
|
||||
folio.reservation_ids.mapped("move_line_ids")
|
||||
.mapped("move_id")
|
||||
.filtered(lambda r: r.type in ["out_invoice", "out_refund"])
|
||||
)
|
||||
invoice_ids = (
|
||||
folio.service_ids.mapped("move_line_ids")
|
||||
.mapped("move_id")
|
||||
.filtered(lambda r: r.type in ["out_invoice", "out_refund"])
|
||||
)
|
||||
# TODO: Search for invoices which have been 'cancelled'
|
||||
# (filter_refund = 'modify' in 'account.move.refund')
|
||||
# use like as origin may contains multiple references
|
||||
# (e.g. 'SO01, SO02')
|
||||
refunds = invoice_ids.search(
|
||||
unconfirmed_orders = self.filtered(lambda so: so.state in ["draft"])
|
||||
unconfirmed_orders.invoice_status = "no"
|
||||
confirmed_orders = self - unconfirmed_orders
|
||||
if not confirmed_orders:
|
||||
return
|
||||
line_invoice_status_all = [
|
||||
(d["folio_id"][0], d["invoice_status"])
|
||||
for d in self.env["folio.sale.line"].read_group(
|
||||
[
|
||||
("invoice_origin", "like", folio.name),
|
||||
("company_id", "=", folio.company_id.id),
|
||||
]
|
||||
).filtered(lambda r: r.type in ["out_invoice", "out_refund"])
|
||||
invoice_ids |= refunds.filtered(lambda r: folio.id in r.folio_ids.ids)
|
||||
# Search for refunds as well
|
||||
refund_ids = self.env["account.move"].browse()
|
||||
if invoice_ids:
|
||||
for inv in invoice_ids:
|
||||
refund_ids += refund_ids.search(
|
||||
[
|
||||
("type", "=", "out_refund"),
|
||||
("invoice_origin", "=", inv.number),
|
||||
("invoice_origin", "!=", False),
|
||||
("journal_id", "=", inv.journal_id.id),
|
||||
]
|
||||
)
|
||||
# Ignore the status of the deposit product
|
||||
deposit_product_id = self.env[
|
||||
"sale.advance.payment.inv"
|
||||
]._default_product_id()
|
||||
service_invoice_status = [
|
||||
service.invoice_status
|
||||
for service in folio.service_ids
|
||||
if service.product_id != deposit_product_id
|
||||
]
|
||||
reservation_invoice_status = [
|
||||
reservation.invoice_status for reservation in folio.reservation_ids
|
||||
]
|
||||
|
||||
if folio.state not in ("confirm", "done"):
|
||||
invoice_status = "no"
|
||||
elif any(
|
||||
invoice_status == "to invoice"
|
||||
for invoice_status in service_invoice_status
|
||||
) or any(
|
||||
invoice_status == "to invoice"
|
||||
for invoice_status in reservation_invoice_status
|
||||
):
|
||||
invoice_status = "to invoice"
|
||||
elif all(
|
||||
invoice_status == "invoiced"
|
||||
for invoice_status in service_invoice_status
|
||||
) or any(
|
||||
invoice_status == "invoiced"
|
||||
for invoice_status in reservation_invoice_status
|
||||
):
|
||||
invoice_status = "invoiced"
|
||||
else:
|
||||
invoice_status = "no"
|
||||
|
||||
folio.update(
|
||||
{
|
||||
"move_ids": move_ids.ids + refund_ids.ids,
|
||||
"invoice_status": invoice_status,
|
||||
}
|
||||
("folio_id", "in", confirmed_orders.ids),
|
||||
("is_downpayment", "=", False),
|
||||
("display_type", "=", False),
|
||||
],
|
||||
["folio_id", "invoice_status"],
|
||||
["folio_id", "invoice_status"],
|
||||
lazy=False,
|
||||
)
|
||||
]
|
||||
for order in confirmed_orders:
|
||||
line_invoice_status = [
|
||||
d[1] for d in line_invoice_status_all if d[0] == order.id
|
||||
]
|
||||
if order.state in ("draft"):
|
||||
order.invoice_status = "no"
|
||||
elif any(
|
||||
invoice_status == "to invoice" for invoice_status in line_invoice_status
|
||||
):
|
||||
order.invoice_status = "to invoice"
|
||||
elif line_invoice_status and all(
|
||||
invoice_status == "invoiced" for invoice_status in line_invoice_status
|
||||
):
|
||||
order.invoice_status = "invoiced"
|
||||
elif line_invoice_status and all(
|
||||
invoice_status in ("invoiced", "upselling")
|
||||
for invoice_status in line_invoice_status
|
||||
):
|
||||
order.invoice_status = "upselling"
|
||||
else:
|
||||
order.invoice_status = "no"
|
||||
|
||||
@api.depends("reservation_ids.price_total", "service_ids.price_total")
|
||||
def _compute_amount_all(self):
|
||||
@@ -514,39 +650,67 @@ class PmsFolio(models.Model):
|
||||
)
|
||||
|
||||
# TODO: Add return_ids to depends
|
||||
@api.depends("amount_total", "payment_ids", "reservation_type", "state")
|
||||
@api.depends(
|
||||
"amount_total",
|
||||
"reservation_type",
|
||||
"state",
|
||||
"move_line_ids",
|
||||
"move_line_ids.parent_state",
|
||||
"sale_line_ids.invoice_lines",
|
||||
"sale_line_ids.invoice_lines.move_id.payment_state",
|
||||
)
|
||||
def _compute_amount(self):
|
||||
acc_pay_obj = self.env["account.payment"]
|
||||
for record in self:
|
||||
if record.reservation_type in ("staff", "out"):
|
||||
vals = {
|
||||
"pending_amount": 0,
|
||||
"invoices_paid": 0,
|
||||
# "refund_amount": 0,
|
||||
}
|
||||
record.update(vals)
|
||||
else:
|
||||
total_inv_refund = 0
|
||||
payments = acc_pay_obj.search([("folio_id", "=", record.id)])
|
||||
total_paid = sum(pay.amount for pay in payments)
|
||||
# return_lines = self.env["payment.return.line"].search(
|
||||
# [
|
||||
# ("move_line_ids", "in", payments.mapped("move_line_ids.id")),
|
||||
# ("return_id.state", "=", "done"),
|
||||
# ]
|
||||
# )
|
||||
# total_inv_refund = sum(
|
||||
# pay_return.amount for pay_return in return_lines
|
||||
# )
|
||||
journals = record.pms_property_id._get_payment_methods()
|
||||
paid_out = 0
|
||||
for journal in journals:
|
||||
paid_out += sum(
|
||||
self.env["account.move.line"]
|
||||
.search(
|
||||
[
|
||||
("folio_ids", "in", record.id),
|
||||
(
|
||||
"account_id",
|
||||
"in",
|
||||
tuple(
|
||||
journal.default_account_id.ids
|
||||
+ journal.payment_debit_account_id.ids
|
||||
+ journal.payment_credit_account_id.ids
|
||||
),
|
||||
),
|
||||
(
|
||||
"display_type",
|
||||
"not in",
|
||||
("line_section", "line_note"),
|
||||
),
|
||||
("move_id.state", "!=", "cancel"),
|
||||
]
|
||||
)
|
||||
.mapped("balance")
|
||||
)
|
||||
total = record.amount_total
|
||||
# REVIEW: Must We ignored services in cancelled folios
|
||||
# pending amount?
|
||||
if record.state == "cancelled":
|
||||
total = total - sum(record.service_ids.mapped("price_total"))
|
||||
# Compute 'payment_state'.
|
||||
if total <= paid_out:
|
||||
payment_state = "paid"
|
||||
elif paid_out < total:
|
||||
payment_state = "partial"
|
||||
else:
|
||||
payment_state = "not_paid"
|
||||
vals = {
|
||||
"pending_amount": total - total_paid + total_inv_refund,
|
||||
"invoices_paid": total_paid,
|
||||
# "refund_amount": total_inv_refund,
|
||||
"pending_amount": total - paid_out,
|
||||
"invoices_paid": paid_out,
|
||||
"payment_state": payment_state,
|
||||
}
|
||||
record.update(vals)
|
||||
|
||||
@@ -554,30 +718,13 @@ class PmsFolio(models.Model):
|
||||
|
||||
def action_pay(self):
|
||||
self.ensure_one()
|
||||
partner = self.partner_id.id
|
||||
amount = self.pending_amount
|
||||
view_id = self.env.ref("pms.account_payment_view_form_folio").id
|
||||
return {
|
||||
"name": _("Register Payment"),
|
||||
"view_type": "form",
|
||||
"view_mode": "form",
|
||||
"res_model": "account.payment",
|
||||
"type": "ir.actions.act_window",
|
||||
"view_id": view_id,
|
||||
"context": {
|
||||
"default_folio_id": self.id,
|
||||
"default_amount": amount,
|
||||
"default_payment_type": "inbound",
|
||||
"default_partner_type": "customer",
|
||||
"default_partner_id": partner,
|
||||
"default_communication": self.name,
|
||||
},
|
||||
"target": "new",
|
||||
}
|
||||
action = self.env.ref("pms.action_payment_folio").sudo().read()[0]
|
||||
action["res_id"] = self.id
|
||||
return action
|
||||
|
||||
def open_moves_folio(self):
|
||||
invoices = self.mapped("move_ids")
|
||||
action = self.env.ref("account.action_move_out_invoice_type").read()[0]
|
||||
action = self.env.ref("account.action_move_out_invoice_type").sudo().read()[0]
|
||||
if len(invoices) > 1:
|
||||
action["domain"] = [("id", "in", invoices.ids)]
|
||||
elif len(invoices) == 1:
|
||||
@@ -685,11 +832,327 @@ class PmsFolio(models.Model):
|
||||
|
||||
# create an analytic account if at least an expense product
|
||||
# if any([expense_policy != 'no' for expense_policy in
|
||||
# self.order_line.mapped('product_id.expense_policy')]):
|
||||
# self.sale_line_ids.mapped('product_id.expense_policy')]):
|
||||
# if not self.analytic_account_id:
|
||||
# self._create_analytic_account()
|
||||
return True
|
||||
|
||||
# CHECKIN/OUT PROCESS
|
||||
|
||||
def _compute_checkin_partner_count(self):
|
||||
for record in self:
|
||||
if record.reservation_type == "normal" and record.reservation_ids:
|
||||
filtered_reservs = record.reservation_ids.filtered(
|
||||
lambda x: x.state != "cancelled"
|
||||
)
|
||||
mapped_checkin_partner = filtered_reservs.mapped(
|
||||
"checkin_partner_ids.id"
|
||||
)
|
||||
record.checkin_partner_count = len(mapped_checkin_partner)
|
||||
mapped_checkin_partner_count = filtered_reservs.mapped(
|
||||
lambda x: (x.adults + x.children) - len(x.checkin_partner_ids)
|
||||
)
|
||||
record.checkin_partner_pending_count = sum(mapped_checkin_partner_count)
|
||||
|
||||
def _prepare_invoice(self):
|
||||
"""
|
||||
Prepare the dict of values to create the new invoice for a folio.
|
||||
This method may be overridden to implement custom invoice generation
|
||||
(making sure to call super() to establish a clean extension chain).
|
||||
"""
|
||||
self.ensure_one()
|
||||
journal = (
|
||||
self.env["account.move"]
|
||||
.with_context(default_move_type="out_invoice")
|
||||
._get_default_journal()
|
||||
)
|
||||
if not journal:
|
||||
raise UserError(
|
||||
_("Please define an accounting sales journal for the company %s (%s).")
|
||||
% (self.company_id.name, self.company_id.id)
|
||||
)
|
||||
|
||||
invoice_vals = {
|
||||
"ref": self.client_order_ref or "",
|
||||
"move_type": "out_invoice",
|
||||
"narration": self.note,
|
||||
"currency_id": self.pricelist_id.currency_id.id,
|
||||
# 'campaign_id': self.campaign_id.id,
|
||||
# 'medium_id': self.medium_id.id,
|
||||
# 'source_id': self.source_id.id,
|
||||
"invoice_user_id": self.user_id and self.user_id.id,
|
||||
"partner_id": self.partner_invoice_id.id,
|
||||
"partner_bank_id": self.company_id.partner_id.bank_ids[:1].id,
|
||||
"journal_id": journal.id, # company comes from the journal
|
||||
"invoice_origin": self.name,
|
||||
"invoice_payment_term_id": self.payment_term_id.id,
|
||||
"payment_reference": self.reference,
|
||||
"transaction_ids": [(6, 0, self.transaction_ids.ids)],
|
||||
"folio_ids": [(6, 0, [self.id])],
|
||||
"invoice_line_ids": [],
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
return invoice_vals
|
||||
|
||||
def action_view_invoice(self):
|
||||
invoices = self.mapped("move_ids")
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
"account.action_move_out_invoice_type"
|
||||
)
|
||||
if len(invoices) > 1:
|
||||
action["domain"] = [("id", "in", invoices.ids)]
|
||||
elif len(invoices) == 1:
|
||||
form_view = [(self.env.ref("account.view_move_form").id, "form")]
|
||||
if "views" in action:
|
||||
action["views"] = form_view + [
|
||||
(state, view) for state, view in action["views"] if view != "form"
|
||||
]
|
||||
else:
|
||||
action["views"] = form_view
|
||||
action["res_id"] = invoices.id
|
||||
else:
|
||||
action = {"type": "ir.actions.act_window_close"}
|
||||
|
||||
context = {
|
||||
"default_move_type": "out_invoice",
|
||||
}
|
||||
if len(self) == 1:
|
||||
context.update(
|
||||
{
|
||||
"default_partner_id": self.partner_id.id,
|
||||
"default_invoice_payment_term_id": self.payment_term_id.id
|
||||
or self.partner_id.property_payment_term_id.id
|
||||
or self.env["account.move"]
|
||||
.default_get(["invoice_payment_term_id"])
|
||||
.get("invoice_payment_term_id"),
|
||||
"default_invoice_origin": self.mapped("name"),
|
||||
"default_user_id": self.user_id.id,
|
||||
}
|
||||
)
|
||||
action["context"] = context
|
||||
return action
|
||||
|
||||
def _get_invoice_grouping_keys(self):
|
||||
return ["company_id", "partner_id", "currency_id"]
|
||||
|
||||
@api.model
|
||||
def _nothing_to_invoice_error(self):
|
||||
msg = _(
|
||||
"""There is nothing to invoice!\n
|
||||
Reason(s) of this behavior could be:
|
||||
- You should deliver your products before invoicing them: Click on the "truck"
|
||||
icon (top-right of your screen) and follow instructions.
|
||||
- You should modify the invoicing policy of your product: Open the product,
|
||||
go to the "Sales tab" and modify invoicing policy from "delivered quantities"
|
||||
to "ordered quantities".
|
||||
"""
|
||||
)
|
||||
return UserError(msg)
|
||||
|
||||
def _create_invoices(
|
||||
self,
|
||||
grouped=False,
|
||||
final=False,
|
||||
date=None,
|
||||
lines_to_invoice=False,
|
||||
):
|
||||
"""
|
||||
Create the invoice associated to the Folio.
|
||||
:param grouped: if True, invoices are grouped by Folio id.
|
||||
If False, invoices are grouped by
|
||||
(partner_invoice_id, currency)
|
||||
:param final: if True, refunds will be generated if necessary
|
||||
: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:
|
||||
self.check_access_rights("write")
|
||||
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)
|
||||
|
||||
if not invoice_vals_list:
|
||||
raise self._nothing_to_invoice_error()
|
||||
|
||||
# 2) Manage 'grouped' parameter: group by (partner_id, currency_id).
|
||||
if not grouped:
|
||||
new_invoice_vals_list = []
|
||||
invoice_grouping_keys = self._get_invoice_grouping_keys()
|
||||
for _grouping_keys, invoices in groupby(
|
||||
invoice_vals_list,
|
||||
key=lambda x: [
|
||||
x.get(grouping_key) for grouping_key in invoice_grouping_keys
|
||||
],
|
||||
):
|
||||
origins = set()
|
||||
payment_refs = set()
|
||||
refs = set()
|
||||
ref_invoice_vals = None
|
||||
for invoice_vals in invoices:
|
||||
if not ref_invoice_vals:
|
||||
ref_invoice_vals = invoice_vals
|
||||
else:
|
||||
ref_invoice_vals["invoice_line_ids"] += invoice_vals[
|
||||
"invoice_line_ids"
|
||||
]
|
||||
origins.add(invoice_vals["invoice_origin"])
|
||||
payment_refs.add(invoice_vals["payment_reference"])
|
||||
refs.add(invoice_vals["ref"])
|
||||
ref_invoice_vals.update(
|
||||
{
|
||||
"ref": ", ".join(refs)[:2000],
|
||||
"invoice_origin": ", ".join(origins),
|
||||
"payment_reference": len(payment_refs) == 1
|
||||
and payment_refs.pop()
|
||||
or False,
|
||||
}
|
||||
)
|
||||
new_invoice_vals_list.append(ref_invoice_vals)
|
||||
invoice_vals_list = new_invoice_vals_list
|
||||
|
||||
# 3) Create invoices.
|
||||
|
||||
# As part of the invoice creation, we make sure the
|
||||
# sequence of multiple SO do not interfere
|
||||
# in a single invoice. Example:
|
||||
# Folio 1:
|
||||
# - Section A (sequence: 10)
|
||||
# - Product A (sequence: 11)
|
||||
# Folio 2:
|
||||
# - Section B (sequence: 10)
|
||||
# - Product B (sequence: 11)
|
||||
#
|
||||
# If Folio 1 & 2 are grouped in the same invoice,
|
||||
# the result will be:
|
||||
# - Section A (sequence: 10)
|
||||
# - Section B (sequence: 10)
|
||||
# - Product A (sequence: 11)
|
||||
# - Product B (sequence: 11)
|
||||
#
|
||||
# Resequencing should be safe, however we resequence only
|
||||
# if there are less invoices than orders, meaning a grouping
|
||||
# might have been done. This could also mean that only a part
|
||||
# of the selected SO are invoiceable, but resequencing
|
||||
# in this case shouldn't be an issue.
|
||||
if len(invoice_vals_list) < len(self):
|
||||
FolioSaleLine = self.env["folio.sale.line"]
|
||||
for invoice in invoice_vals_list:
|
||||
sequence = 1
|
||||
for line in invoice["invoice_line_ids"]:
|
||||
line[2]["sequence"] = FolioSaleLine._get_invoice_line_sequence(
|
||||
new=sequence, old=line[2]["sequence"]
|
||||
)
|
||||
sequence += 1
|
||||
|
||||
# Manage the creation of invoices in sudo because
|
||||
# a salesperson must be able to generate an invoice from a
|
||||
# sale order without "billing" access rights.
|
||||
# However, he should not be able to create an invoice from scratch.
|
||||
moves = (
|
||||
self.env["account.move"]
|
||||
.sudo()
|
||||
.with_context(default_move_type="out_invoice")
|
||||
.create(invoice_vals_list)
|
||||
)
|
||||
|
||||
# 4) Some moves might actually be refunds: convert
|
||||
# them if the total amount is negative
|
||||
# We do this after the moves have been created
|
||||
# since we need taxes, etc. to know if the total
|
||||
# is actually negative or not
|
||||
if final:
|
||||
moves.sudo().filtered(
|
||||
lambda m: m.amount_total < 0
|
||||
).action_switch_invoice_into_refund_credit_note()
|
||||
for move in moves:
|
||||
move.message_post_with_view(
|
||||
"mail.message_origin_link",
|
||||
values={
|
||||
"self": move,
|
||||
"origin": move.line_ids.mapped("folio_line_ids.folio_id"),
|
||||
},
|
||||
subtype_id=self.env.ref("mail.mt_note").id,
|
||||
)
|
||||
return moves
|
||||
|
||||
def get_invoice_vals_list(self, final=False, lines_to_invoice=False):
|
||||
precision = self.env["decimal.precision"].precision_get(
|
||||
"Product Unit of Measure"
|
||||
)
|
||||
invoice_vals_list = []
|
||||
invoice_item_sequence = 0
|
||||
for order in self:
|
||||
order = order.with_company(order.company_id)
|
||||
current_section_vals = None
|
||||
down_payments = order.env["folio.sale.line"]
|
||||
|
||||
# Invoice values.
|
||||
invoice_vals = order._prepare_invoice()
|
||||
|
||||
# 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
|
||||
):
|
||||
if line.display_type == "line_section":
|
||||
current_section_vals = line._prepare_invoice_line(
|
||||
sequence=invoice_item_sequence + 1
|
||||
)
|
||||
continue
|
||||
if line.display_type != "line_note" and float_is_zero(
|
||||
line.qty_to_invoice, precision_digits=precision
|
||||
):
|
||||
continue
|
||||
if (
|
||||
line.qty_to_invoice > 0
|
||||
or (line.qty_to_invoice < 0 and final)
|
||||
or line.display_type == "line_note"
|
||||
):
|
||||
if line.is_downpayment:
|
||||
down_payments += line
|
||||
continue
|
||||
if current_section_vals:
|
||||
invoice_item_sequence += 1
|
||||
invoice_lines_vals.append(current_section_vals)
|
||||
current_section_vals = None
|
||||
invoice_item_sequence += 1
|
||||
prepared_line = line._prepare_invoice_line(
|
||||
sequence=invoice_item_sequence
|
||||
)
|
||||
invoice_lines_vals.append(prepared_line)
|
||||
|
||||
# If down payments are present in SO, group them under common section
|
||||
if down_payments:
|
||||
invoice_item_sequence += 1
|
||||
down_payments_section = order._prepare_down_payment_section_line(
|
||||
sequence=invoice_item_sequence
|
||||
)
|
||||
invoice_lines_vals.append(down_payments_section)
|
||||
for down_payment in down_payments:
|
||||
invoice_item_sequence += 1
|
||||
invoice_down_payment_vals = down_payment._prepare_invoice_line(
|
||||
sequence=invoice_item_sequence
|
||||
)
|
||||
invoice_lines_vals.append(invoice_down_payment_vals)
|
||||
|
||||
if not any(
|
||||
new_line["display_type"] is False for new_line in invoice_lines_vals
|
||||
):
|
||||
raise self._nothing_to_invoice_error()
|
||||
|
||||
invoice_vals["invoice_line_ids"] = [
|
||||
(0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals
|
||||
]
|
||||
|
||||
invoice_vals_list.append(invoice_vals)
|
||||
return invoice_vals_list
|
||||
|
||||
def _get_tax_amount_by_group(self):
|
||||
self.ensure_one()
|
||||
res = {}
|
||||
@@ -730,3 +1193,25 @@ class PmsFolio(models.Model):
|
||||
for record in self:
|
||||
if record.agency_id and record.channel_type_id:
|
||||
raise models.ValidationError(_("There must be only one sale channel"))
|
||||
|
||||
@api.model
|
||||
def _prepare_down_payment_section_line(self, **optional_values):
|
||||
"""
|
||||
Prepare the dict of values to create a new down
|
||||
payment section for a sales order line.
|
||||
:param optional_values: any parameter that should
|
||||
be added to the returned down payment section
|
||||
"""
|
||||
down_payments_section_line = {
|
||||
"display_type": "line_section",
|
||||
"name": _("Down Payments"),
|
||||
"product_id": False,
|
||||
"product_uom_id": False,
|
||||
"quantity": 0,
|
||||
"discount": 0,
|
||||
"price_unit": 0,
|
||||
"account_id": False,
|
||||
}
|
||||
if optional_values:
|
||||
down_payments_section_line.update(optional_values)
|
||||
return down_payments_section_line
|
||||
|
||||
@@ -108,3 +108,22 @@ class PmsProperty(models.Model):
|
||||
date = date.astimezone(pytz.utc)
|
||||
date = date.replace(tzinfo=None)
|
||||
return date
|
||||
|
||||
def _get_payment_methods(self):
|
||||
self.ensure_one()
|
||||
payment_methods = self.env["account.journal"].search(
|
||||
[
|
||||
"&",
|
||||
("type", "in", ["cash", "bank"]),
|
||||
"|",
|
||||
("pms_property_ids", "in", self.id),
|
||||
"|",
|
||||
"&",
|
||||
("pms_property_ids", "=", False),
|
||||
("company_id", "=", self.company_id.id),
|
||||
"&",
|
||||
("pms_property_ids", "=", False),
|
||||
("company_id", "=", False),
|
||||
]
|
||||
)
|
||||
return payment_methods
|
||||
|
||||
@@ -84,20 +84,6 @@ class PmsReservation(models.Model):
|
||||
segmentation_ids = folio.segmentation_ids
|
||||
return segmentation_ids
|
||||
|
||||
@api.model
|
||||
def _default_diff_invoicing(self):
|
||||
"""
|
||||
If the guest has an invoicing address set,
|
||||
this method return diff_invoicing = True, else, return False
|
||||
"""
|
||||
if "reservation_id" in self.env.context:
|
||||
reservation = self.env["pms.reservation"].browse(
|
||||
[self.env.context["reservation_id"]]
|
||||
)
|
||||
if reservation.partner_id.id == reservation.partner_invoice_id.id:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Fields declaration
|
||||
name = fields.Text(
|
||||
"Reservation Description",
|
||||
@@ -113,6 +99,7 @@ class PmsReservation(models.Model):
|
||||
string="Room",
|
||||
ondelete="restrict",
|
||||
domain="[('id', 'in', allowed_room_ids)]",
|
||||
copy=False,
|
||||
)
|
||||
allowed_room_ids = fields.Many2many(
|
||||
"pms.room",
|
||||
@@ -124,6 +111,7 @@ class PmsReservation(models.Model):
|
||||
string="Folio",
|
||||
tracking=True,
|
||||
ondelete="restrict",
|
||||
copy=False,
|
||||
)
|
||||
board_service_room_id = fields.Many2one(
|
||||
"pms.board.service.room.type",
|
||||
@@ -137,6 +125,7 @@ class PmsReservation(models.Model):
|
||||
compute="_compute_room_type_id",
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=False,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
"res.partner",
|
||||
@@ -178,6 +167,7 @@ class PmsReservation(models.Model):
|
||||
compute="_compute_reservation_line_ids",
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=False,
|
||||
)
|
||||
service_ids = fields.One2many(
|
||||
"pms.service",
|
||||
@@ -212,6 +202,7 @@ class PmsReservation(models.Model):
|
||||
compute="_compute_checkin_partner_ids",
|
||||
store=True,
|
||||
readonly=False,
|
||||
copy=False,
|
||||
)
|
||||
count_pending_arrival = fields.Integer(
|
||||
"Pending Arrival",
|
||||
@@ -249,13 +240,16 @@ class PmsReservation(models.Model):
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency",
|
||||
related="pricelist_id.currency_id",
|
||||
string="Currency",
|
||||
depends=["pricelist_id"],
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
tax_ids = fields.Many2many(
|
||||
"account.tax",
|
||||
string="Taxes",
|
||||
compute="_compute_tax_ids",
|
||||
readonly="False",
|
||||
store=True,
|
||||
ondelete="restrict",
|
||||
domain=["|", ("active", "=", False), ("active", "=", True)],
|
||||
)
|
||||
@@ -267,9 +261,10 @@ class PmsReservation(models.Model):
|
||||
string="Invoice Lines",
|
||||
copy=False,
|
||||
)
|
||||
analytic_tag_ids = fields.Many2many("account.analytic.tag", string="Analytic Tags")
|
||||
localizator = fields.Char(
|
||||
string="Localizator", compute="_compute_localizator", store=True
|
||||
string="Localizator",
|
||||
compute="_compute_localizator",
|
||||
store=True,
|
||||
)
|
||||
adults = fields.Integer(
|
||||
"Adults",
|
||||
@@ -282,7 +277,6 @@ class PmsReservation(models.Model):
|
||||
children_occupying = fields.Integer(
|
||||
string="Children occupying",
|
||||
)
|
||||
|
||||
children = fields.Integer(
|
||||
"Children",
|
||||
readonly=False,
|
||||
@@ -315,23 +309,26 @@ class PmsReservation(models.Model):
|
||||
compute="_compute_splitted",
|
||||
store=True,
|
||||
)
|
||||
|
||||
rooms = fields.Char(
|
||||
string="Room/s",
|
||||
compute="_compute_rooms",
|
||||
store=True,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
credit_card_details = fields.Text(related="folio_id.credit_card_details")
|
||||
cancelled_reason = fields.Selection(
|
||||
[("late", "Late"), ("intime", "In time"), ("noshow", "No Show")],
|
||||
string="Cause of cancelled",
|
||||
tracking=True,
|
||||
copy=False,
|
||||
)
|
||||
out_service_description = fields.Text("Cause of out of service")
|
||||
checkin = fields.Date("Check In", required=True, default=_get_default_checkin)
|
||||
checkout = fields.Date("Check Out", required=True, default=_get_default_checkout)
|
||||
checkin = fields.Date(
|
||||
"Check In", required=True, default=_get_default_checkin, copy=False
|
||||
)
|
||||
checkout = fields.Date(
|
||||
"Check Out", required=True, default=_get_default_checkout, copy=False
|
||||
)
|
||||
arrival_hour = fields.Char(
|
||||
"Arrival Hour",
|
||||
default=_get_default_arrival_hour,
|
||||
@@ -350,11 +347,6 @@ class PmsReservation(models.Model):
|
||||
"Exact Departure",
|
||||
compute="_compute_checkout_datetime",
|
||||
)
|
||||
# TODO: As checkin_partner_count is a computed field, it can't not
|
||||
# be used in a domain filer Non-stored field
|
||||
# pms.reservation.checkin_partner_count cannot be searched
|
||||
# searching on a computed field can also be enabled by setting the
|
||||
# search parameter. The value is a method name returning a Domains
|
||||
checkin_partner_count = fields.Integer(
|
||||
"Checkin counter", compute="_compute_checkin_partner_count"
|
||||
)
|
||||
@@ -363,8 +355,16 @@ class PmsReservation(models.Model):
|
||||
compute="_compute_checkin_partner_count",
|
||||
search="_search_checkin_partner_pending",
|
||||
)
|
||||
overbooking = fields.Boolean("Is Overbooking", default=False)
|
||||
reselling = fields.Boolean("Is Reselling", default=False)
|
||||
overbooking = fields.Boolean(
|
||||
"Is Overbooking",
|
||||
default=False,
|
||||
copy=False,
|
||||
)
|
||||
reselling = fields.Boolean(
|
||||
"Is Reselling",
|
||||
default=False,
|
||||
copy=False,
|
||||
)
|
||||
nights = fields.Integer("Nights", compute="_compute_nights", store=True)
|
||||
origin = fields.Char("Origin", compute="_compute_origin", store=True)
|
||||
detail_origin = fields.Char(
|
||||
@@ -380,11 +380,13 @@ class PmsReservation(models.Model):
|
||||
string="Internal Partner Notes", related="partner_id.comment"
|
||||
)
|
||||
folio_internal_comment = fields.Text(
|
||||
string="Internal Folio Notes", related="folio_id.internal_comment"
|
||||
string="Internal Folio Notes",
|
||||
related="folio_id.internal_comment",
|
||||
)
|
||||
preconfirm = fields.Boolean("Auto confirm to Save", default=True)
|
||||
invoice_status = fields.Selection(
|
||||
[
|
||||
("upselling", "Upselling Opportunity"),
|
||||
("invoiced", "Fully Invoiced"),
|
||||
("to invoice", "To Invoice"),
|
||||
("no", "Nothing to Invoice"),
|
||||
@@ -396,19 +398,39 @@ class PmsReservation(models.Model):
|
||||
default="no",
|
||||
)
|
||||
qty_to_invoice = fields.Float(
|
||||
compute="_compute_get_to_invoice_qty",
|
||||
string="To Invoice",
|
||||
compute="_compute_qty_to_invoice",
|
||||
string="To Invoice Quantity",
|
||||
store=True,
|
||||
readonly=True,
|
||||
digits=("Product Unit of Measure"),
|
||||
)
|
||||
qty_invoiced = fields.Float(
|
||||
compute="_compute_get_invoice_qty",
|
||||
string="Invoiced",
|
||||
compute="_compute_qty_invoiced",
|
||||
string="Invoiced Quantity",
|
||||
store=True,
|
||||
readonly=True,
|
||||
digits=("Product Unit of Measure"),
|
||||
)
|
||||
untaxed_amount_invoiced = fields.Monetary(
|
||||
"Untaxed Invoiced Amount",
|
||||
compute="_compute_untaxed_amount_invoiced",
|
||||
compute_sudo=True,
|
||||
store=True,
|
||||
)
|
||||
untaxed_amount_to_invoice = fields.Monetary(
|
||||
"Untaxed Amount To Invoice",
|
||||
compute="_compute_untaxed_amount_to_invoice",
|
||||
compute_sudo=True,
|
||||
store=True,
|
||||
)
|
||||
analytic_tag_ids = fields.Many2many(
|
||||
"account.analytic.tag",
|
||||
string="Analytic Tags",
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
)
|
||||
analytic_line_ids = fields.One2many(
|
||||
"account.analytic.line", "so_line", string="Analytic lines"
|
||||
)
|
||||
price_subtotal = fields.Monetary(
|
||||
string="Subtotal",
|
||||
readonly=True,
|
||||
@@ -443,6 +465,7 @@ class PmsReservation(models.Model):
|
||||
string="Discount (€)",
|
||||
digits=("Discount"),
|
||||
compute="_compute_discount",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
|
||||
@@ -532,17 +555,18 @@ class PmsReservation(models.Model):
|
||||
)
|
||||
reservation.allowed_room_ids = rooms_available
|
||||
|
||||
@api.depends("reservation_type", "agency_id")
|
||||
@api.depends("reservation_type", "agency_id", "folio_id")
|
||||
def _compute_partner_id(self):
|
||||
for reservation in self:
|
||||
if reservation.reservation_type == "out":
|
||||
reservation.partner_id = reservation.pms_property_id.partner_id.id
|
||||
if reservation.folio_id:
|
||||
reservation.partner_id = reservation.folio_id.partner_id
|
||||
else:
|
||||
reservation.partner_id = False
|
||||
if not reservation.partner_id and reservation.agency_id:
|
||||
reservation.partner_id = reservation.agency_id
|
||||
elif not reservation.partner_id:
|
||||
if reservation.folio_id:
|
||||
reservation.partner_id = reservation.folio_id.partner_id
|
||||
elif reservation.agency_id:
|
||||
reservation.partner_id = reservation.agency_id
|
||||
else:
|
||||
reservation.partner_id = False
|
||||
|
||||
@api.depends("partner_id")
|
||||
def _compute_partner_invoice_id(self):
|
||||
@@ -851,11 +875,10 @@ class PmsReservation(models.Model):
|
||||
line.invoice_status = "no"
|
||||
|
||||
@api.depends("qty_invoiced", "nights", "folio_id.state")
|
||||
def _compute_get_to_invoice_qty(self):
|
||||
def _compute_qty_to_invoice(self):
|
||||
"""
|
||||
Compute the quantity to invoice. If the invoice policy is order,
|
||||
the quantity to invoice is calculated from the ordered quantity.
|
||||
Otherwise, the quantity delivered is used.
|
||||
Compute the quantity to invoice. The quantity to invoice is
|
||||
calculated from the nights quantity.
|
||||
"""
|
||||
for line in self:
|
||||
if line.folio_id.state not in ["draft"]:
|
||||
@@ -863,25 +886,138 @@ class PmsReservation(models.Model):
|
||||
else:
|
||||
line.qty_to_invoice = 0
|
||||
|
||||
@api.depends("move_line_ids.move_id.state", "move_line_ids.quantity")
|
||||
def _compute_get_invoice_qty(self):
|
||||
@api.depends(
|
||||
"move_line_ids.move_id.state",
|
||||
"move_line_ids.quantity",
|
||||
"untaxed_amount_to_invoice",
|
||||
)
|
||||
def _compute_qty_invoiced(self):
|
||||
"""
|
||||
Compute the quantity invoiced. If case of a refund, the quantity
|
||||
invoiced is decreased. We must check day per day and sum or
|
||||
decreased on 1 unit per invoice_line
|
||||
"""
|
||||
for line in self:
|
||||
for record in self:
|
||||
qty_invoiced = 0.0
|
||||
for day in line.reservation_line_ids:
|
||||
invoice_lines = day.move_line_ids.filtered(
|
||||
for line in record.reservation_line_ids:
|
||||
invoice_lines = line.move_line_ids.filtered(
|
||||
lambda r: r.move_id.state != "cancel"
|
||||
)
|
||||
qty_invoiced += len(
|
||||
invoice_lines.filtered(lambda r: r.move_id.type == "out_invoice")
|
||||
invoice_lines.filtered(
|
||||
lambda r: r.move_id.move_type == "out_invoice"
|
||||
)
|
||||
) - len(
|
||||
invoice_lines.filtered(lambda r: r.move_id.type == "out_refund")
|
||||
invoice_lines.filtered(
|
||||
lambda r: r.move_id.move_type == "out_refund"
|
||||
)
|
||||
)
|
||||
line.qty_invoiced = qty_invoiced
|
||||
record.qty_invoiced = qty_invoiced
|
||||
|
||||
@api.depends(
|
||||
"move_line_ids",
|
||||
"move_line_ids.price_total",
|
||||
"move_line_ids.move_id.state",
|
||||
"move_line_ids.move_id.move_type",
|
||||
)
|
||||
def _compute_untaxed_amount_invoiced(self):
|
||||
"""Compute the untaxed amount already invoiced from the reservation,
|
||||
taking the refund attached
|
||||
the reservation into account. This amount is computed as
|
||||
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
|
||||
where
|
||||
`inv_line` is a customer invoice line linked to the reservation
|
||||
`ref_line` is a customer credit note (refund)
|
||||
line linked to the reservation
|
||||
"""
|
||||
for line in self:
|
||||
amount_invoiced = 0.0
|
||||
for invoice_line in line.move_line_ids:
|
||||
if invoice_line.move_id.state == "posted":
|
||||
invoice_date = (
|
||||
invoice_line.move_id.invoice_date or fields.Date.today()
|
||||
)
|
||||
if invoice_line.move_id.move_type == "out_invoice":
|
||||
amount_invoiced += invoice_line.currency_id._convert(
|
||||
invoice_line.price_subtotal,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
invoice_date,
|
||||
)
|
||||
elif invoice_line.move_id.move_type == "out_refund":
|
||||
amount_invoiced -= invoice_line.currency_id._convert(
|
||||
invoice_line.price_subtotal,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
invoice_date,
|
||||
)
|
||||
line.untaxed_amount_invoiced = amount_invoiced
|
||||
|
||||
@api.depends(
|
||||
"state", "discount", "price_total", "room_type_id", "untaxed_amount_invoiced"
|
||||
)
|
||||
def _compute_untaxed_amount_to_invoice(self):
|
||||
"""Total of remaining amount to invoice on the reservation (taxes excl.) as
|
||||
total_sol - amount already invoiced
|
||||
|
||||
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
|
||||
come only from the reservation.
|
||||
"""
|
||||
for line in self:
|
||||
amount_to_invoice = 0.0
|
||||
if line.state not in ["draft"]:
|
||||
price_subtotal = 0.0
|
||||
price_subtotal = line.price_subtotal
|
||||
if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0:
|
||||
# As included taxes are not excluded from the computed subtotal,
|
||||
# `compute_all()` method
|
||||
# has to be called to retrieve the subtotal without them.
|
||||
price_subtotal = line.tax_ids.compute_all(
|
||||
price_subtotal,
|
||||
currency=line.currency_id,
|
||||
quantity=line.nights,
|
||||
product=line.room_type_id.product_id,
|
||||
)["total_excluded"]
|
||||
|
||||
if any(
|
||||
line.move_line_ids.mapped(lambda i: i.discount != line.discount)
|
||||
):
|
||||
# In case of re-invoicing with different discount we
|
||||
# try to calculate manually the
|
||||
# remaining amount to invoice
|
||||
amount = 0
|
||||
for move in line.move_line_ids:
|
||||
if (
|
||||
len(move.tax_ids.filtered(lambda tax: tax.price_include))
|
||||
> 0
|
||||
):
|
||||
amount += move.tax_ids.compute_all(
|
||||
move.currency_id._convert(
|
||||
move.price_unit,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
move.date or fields.Date.today(),
|
||||
round=False,
|
||||
)
|
||||
* move.quantity
|
||||
)["total_excluded"]
|
||||
else:
|
||||
amount += (
|
||||
move.currency_id._convert(
|
||||
move.price_unit,
|
||||
line.currency_id,
|
||||
line.company_id,
|
||||
move.date or fields.Date.today(),
|
||||
round=False,
|
||||
)
|
||||
* move.quantity
|
||||
)
|
||||
|
||||
amount_to_invoice = max(price_subtotal - amount, 0)
|
||||
else:
|
||||
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
|
||||
|
||||
line.untaxed_amount_to_invoice = amount_to_invoice
|
||||
|
||||
@api.depends("reservation_line_ids")
|
||||
def _compute_nights(self):
|
||||
@@ -1033,33 +1169,10 @@ class PmsReservation(models.Model):
|
||||
if record.agency_id and not record.agency_id.is_agency:
|
||||
raise ValidationError(_("booking agency with wrong configuration: "))
|
||||
|
||||
# @api.constrains("reservation_type", "partner_id")
|
||||
# def _check_partner_reservation(self):
|
||||
# for reservation in self:
|
||||
# if (
|
||||
# reservation.reservation_type == "out"
|
||||
# and reservation.partner_id.id != \
|
||||
# reservation.pms_property_id.partner_id.id
|
||||
# ):
|
||||
# raise models.ValidationError(
|
||||
# _("The partner on out reservations must be a property partner")
|
||||
# )
|
||||
|
||||
# @api.constrains("closure_reason_id", "reservation_type")
|
||||
# def _check_clousure_reservation(self):
|
||||
# for reservation in self:
|
||||
# if reservation.closure_reason_id and \
|
||||
# reservation.reservation_type != "out":
|
||||
# raise models.ValidationError(
|
||||
# _("Only the out reservations can has a clousure reason")
|
||||
# )
|
||||
|
||||
# self._compute_tax_ids() TODO: refact
|
||||
|
||||
# Action methods
|
||||
|
||||
def open_folio(self):
|
||||
action = self.env.ref("pms.open_pms_folio1_form_tree_all").read()[0]
|
||||
action = self.env.ref("pms.open_pms_folio1_form_tree_all").sudo().read()[0]
|
||||
if self.folio_id:
|
||||
action["views"] = [(self.env.ref("pms.pms_folio_view_form").id, "form")]
|
||||
action["res_id"] = self.folio_id.id
|
||||
@@ -1068,7 +1181,7 @@ class PmsReservation(models.Model):
|
||||
return action
|
||||
|
||||
def open_reservation_form(self):
|
||||
action = self.env.ref("pms.open_pms_reservation_form_tree_all").read()[0]
|
||||
action = self.env.ref("pms.open_pms_reservation_form_tree_all").sudo().read()[0]
|
||||
action["views"] = [(self.env.ref("pms.pms_reservation_view_form").id, "form")]
|
||||
action["res_id"] = self.id
|
||||
return action
|
||||
@@ -1146,6 +1259,19 @@ class PmsReservation(models.Model):
|
||||
result.append((res.id, name))
|
||||
return result
|
||||
|
||||
def copy_data(self, default=None):
|
||||
rooms_available = self.env["pms.room.type.availability.plan"].rooms_available(
|
||||
self.checkin,
|
||||
self.checkout,
|
||||
room_type_id=self.room_type_id.id,
|
||||
pricelist=self.pricelist_id.id,
|
||||
)
|
||||
if self.preferred_room_id.id in rooms_available.ids:
|
||||
default["preferred_room_id"] = self.preferred_room_id.id
|
||||
if self.room_type_id.id in rooms_available.mapped("room_type_id.id"):
|
||||
default["room_type_id"] = self.room_type_id.id
|
||||
return super(PmsReservation, self).copy_data(default)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if "folio_id" in vals:
|
||||
@@ -1405,19 +1531,15 @@ class PmsReservation(models.Model):
|
||||
if reservation.checkout_datetime <= fields.Datetime.now():
|
||||
reservations.state = "no_checkout"
|
||||
|
||||
def unify(self):
|
||||
# TODO
|
||||
return True
|
||||
|
||||
@api.depends("room_type_id")
|
||||
def _compute_tax_ids(self):
|
||||
for record in self:
|
||||
# If company_id is set, always filter taxes by the company
|
||||
folio = record.folio_id or self.env.context.get("default_folio_id")
|
||||
record = record.with_company(record.company_id)
|
||||
product = self.env["product.product"].browse(
|
||||
record.room_type_id.product_id.id
|
||||
)
|
||||
record.tax_ids = product.taxes_id.filtered(
|
||||
lambda r: not record.company_id or r.company_id == folio.company_id
|
||||
lambda t: t.company_id == record.env.company
|
||||
)
|
||||
|
||||
@api.depends("reservation_line_ids", "reservation_line_ids.room_id")
|
||||
|
||||
@@ -65,6 +65,11 @@ class PmsReservationLine(models.Model):
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
invoiced = fields.Boolean(
|
||||
string="Invoiced",
|
||||
compute="_compute_invoiced",
|
||||
store=True,
|
||||
)
|
||||
cancel_discount = fields.Float(
|
||||
string="Cancel Discount (%)",
|
||||
digits=("Discount"),
|
||||
@@ -116,7 +121,7 @@ class PmsReservationLine(models.Model):
|
||||
room_type_id=reservation.room_type_id.id
|
||||
if not free_room_select
|
||||
else False,
|
||||
current_lines=line._origin.reservation_id.reservation_line_ids.ids,
|
||||
current_lines=line.reservation_id.reservation_line_ids.ids,
|
||||
pricelist=line.reservation_id.pricelist_id.id,
|
||||
)
|
||||
# if there is availability for the entire stay
|
||||
@@ -282,8 +287,6 @@ class PmsReservationLine(models.Model):
|
||||
line.reservation_id.company_id,
|
||||
)
|
||||
# TODO: Out of service 0 amount
|
||||
else:
|
||||
line.price = line._origin.price
|
||||
|
||||
@api.depends("reservation_id.state", "reservation_id.overbooking")
|
||||
def _compute_occupies_availability(self):
|
||||
@@ -315,6 +318,18 @@ class PmsReservationLine(models.Model):
|
||||
return True
|
||||
return False
|
||||
|
||||
@api.depends("move_line_ids", "move_line_ids.move_id.state")
|
||||
def _compute_invoiced(self):
|
||||
for line in self:
|
||||
qty_invoiced = 0
|
||||
for invoice_line in line.move_line_ids:
|
||||
if invoice_line.move_id.state != "cancel":
|
||||
if invoice_line.move_id.move_type == "out_invoice":
|
||||
qty_invoiced += 1
|
||||
elif invoice_line.move_id.move_type == "out_refund":
|
||||
qty_invoiced -= 1
|
||||
line.invoiced = False if qty_invoiced < 1 else True
|
||||
|
||||
# TODO: Refact method and allowed cancelled single days
|
||||
@api.depends("reservation_id.cancelled_reason")
|
||||
def _compute_cancel_discount(self):
|
||||
|
||||
@@ -30,8 +30,8 @@ class PmsRoom(models.Model):
|
||||
name = fields.Char("Room Name", required=True)
|
||||
pms_property_id = fields.Many2one(
|
||||
"pms.property",
|
||||
store=True,
|
||||
readonly=True,
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
)
|
||||
room_type_id = fields.Many2one(
|
||||
"pms.room.type", "Property Room Type", required=True, ondelete="restrict"
|
||||
|
||||
@@ -424,14 +424,20 @@ class PmsService(models.Model):
|
||||
qty_invoiced = 0.0
|
||||
for invoice_line in line.move_line_ids:
|
||||
if invoice_line.move_id.state != "cancel":
|
||||
if invoice_line.move_id.type == "out_invoice":
|
||||
qty_invoiced += invoice_line.uom_id._compute_quantity(
|
||||
invoice_line.quantity, line.product_id.uom_id
|
||||
)
|
||||
elif invoice_line.move_id.type == "out_refund":
|
||||
qty_invoiced -= invoice_line.uom_id._compute_quantity(
|
||||
if invoice_line.move_id.move_type == "out_invoice":
|
||||
qty_invoiced += invoice_line.product_uom_id._compute_quantity(
|
||||
invoice_line.quantity, line.product_id.uom_id
|
||||
)
|
||||
elif invoice_line.move_id.move_type == "out_refund":
|
||||
if (
|
||||
not line.is_downpayment
|
||||
or line.untaxed_amount_to_invoice == 0
|
||||
):
|
||||
qty_invoiced -= (
|
||||
invoice_line.product_uom_id._compute_quantity(
|
||||
invoice_line.quantity, line.product_id.uom_id
|
||||
)
|
||||
)
|
||||
line.qty_invoiced = qty_invoiced
|
||||
|
||||
@api.depends("product_qty", "qty_to_invoice", "qty_invoiced")
|
||||
@@ -496,7 +502,7 @@ class PmsService(models.Model):
|
||||
|
||||
# Action methods
|
||||
def open_service_ids(self):
|
||||
action = self.env.ref("pms.action_pms_services_form").read()[0]
|
||||
action = self.env.ref("pms.action_pms_services_form").sudo().read()[0]
|
||||
action["views"] = [(self.env.ref("pms.pms_service_view_form").id, "form")]
|
||||
action["res_id"] = self.id
|
||||
action["target"] = "new"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Copyright 2017 Alexandre Díaz, Pablo Quesada, Darío Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import logging
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProductPricelist(models.Model):
|
||||
"""Before creating a 'daily' pricelist, you need to consider the following:
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span t-if="doc.payment_ids">
|
||||
<!-- <span t-if="doc.payment_ids">
|
||||
<table style="width:80%;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -288,7 +288,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
</span> -->
|
||||
<!-- <span t-if="doc.return_ids">
|
||||
<table style="width:80%;">
|
||||
<thead>
|
||||
|
||||
@@ -53,3 +53,8 @@ user_access_pms_advanced_filters_wizard,user_access_pms_advanced_filters_wizard,
|
||||
user_access_pms_folio_wizard,user_access_pms_folio_wizard,model_pms_folio_wizard,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_folio_availability_wizard,user_access_pms_folio_availability_wizard,model_pms_folio_availability_wizard,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_num_rooms_selection,user_access_pms_num_rooms_selection,model_pms_num_rooms_selection,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_folio_sale_line,user_access_pms_folio_sale_line,model_folio_sale_line,pms.group_pms_user,1,0,0,0
|
||||
user_access_folio_make_invoice_advance,user_access_folio_make_invoice_advance,model_folio_advance_payment_inv,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_invoice_filter_days,user_access_pms_invoice_filter_days,model_pms_invoice_filter_days,pms.group_pms_user,1,1,1,1
|
||||
user_access_pms_invoice_filter_days_items,user_access_pms_invoice_filter_days_items,model_pms_invoice_filter_days_items,pms.group_pms_user,1,1,1,1
|
||||
user_access_wizard_payment_folio,user_access_wizard_payment_folio,model_wizard_payment_folio,pms.group_pms_user,1,1,1,1
|
||||
|
||||
|
@@ -118,7 +118,6 @@ class TestPmsCheckinPartner(TestHotel):
|
||||
"checkin": "2012-01-15",
|
||||
}
|
||||
)
|
||||
|
||||
# ACT & ASSERT
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
self.checkin1.action_on_board()
|
||||
|
||||
43
pms/tests/test_pms_folio_invoice.py
Normal file
43
pms/tests/test_pms_folio_invoice.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestPmsFolioInvoice(SavepointCase):
|
||||
def setUp(self):
|
||||
super(TestPmsFolioInvoice, self).setUp()
|
||||
|
||||
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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
""" Test create and invoice the cron by partner preconfig automation """
|
||||
|
||||
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):
|
||||
"""Test create with a discount and check discount applied
|
||||
on both Folio lines and an inovoice lines"""
|
||||
|
||||
def test_reinvoice(self):
|
||||
"""Test the compute reinvoice folio take into account
|
||||
nights and services qty invoiced"""
|
||||
10
pms/tests/test_pms_folio_prices.py
Normal file
10
pms/tests/test_pms_folio_prices.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestPmsFolioPrice(SavepointCase):
|
||||
def setUp(self):
|
||||
super(TestPmsFolioPrice, self).setUp()
|
||||
|
||||
def test_price_folio(self):
|
||||
"""Test create reservation and services, and check price
|
||||
tax and discounts"""
|
||||
10
pms/tests/test_pms_invoice_refund.py
Normal file
10
pms/tests/test_pms_invoice_refund.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
freeze_time("2000-02-02")
|
||||
|
||||
|
||||
class TestPmsInvoiceRefund(SavepointCase):
|
||||
def setUp(self):
|
||||
super(TestPmsInvoiceRefund, self).setUp()
|
||||
10
pms/tests/test_pms_payment.py
Normal file
10
pms/tests/test_pms_payment.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
freeze_time("2000-02-02")
|
||||
|
||||
|
||||
class TestPmsPayment(SavepointCase):
|
||||
def setUp(self):
|
||||
super(TestPmsPayment, self).setUp()
|
||||
@@ -126,7 +126,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-16 00:00:00")
|
||||
@@ -179,7 +179,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item1.fixed_price * n_days
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-05 00:00:00")
|
||||
@@ -233,7 +233,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item2.fixed_price * n_days
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-20 00:00:00")
|
||||
@@ -301,7 +301,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item1.fixed_price
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-25 00:00:00")
|
||||
@@ -369,7 +369,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item3.fixed_price
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-02-01 00:00:00")
|
||||
@@ -437,7 +437,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item3.fixed_price * n_days
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-02-01 00:00:00")
|
||||
@@ -492,7 +492,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item1.fixed_price * n_days
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-02-10 00:00:00")
|
||||
@@ -547,7 +547,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -571,7 +571,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -603,10 +603,9 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
)
|
||||
n_days = (reservation.checkout - reservation.checkin).days
|
||||
expected_price = self.item1.fixed_price * n_days
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -641,7 +640,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -689,7 +688,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
expected_price = self.item2.fixed_price * n_days
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -738,7 +737,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -790,7 +789,7 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
@freeze_time("2000-01-01 00:00:00")
|
||||
@@ -856,5 +855,5 @@ class TestPmsPricelistRules(common.TransactionCase):
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(
|
||||
expected_price, reservation.price_total, "The price is not as expected"
|
||||
expected_price, reservation.price_subtotal, "The price is not as expected"
|
||||
)
|
||||
|
||||
12
pms/tests/test_pms_reservation_prices.py
Normal file
12
pms/tests/test_pms_reservation_prices.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestPmsFolioInvoice(SavepointCase):
|
||||
def setUp(self):
|
||||
super(TestPmsFolioInvoice, self).setUp()
|
||||
|
||||
def test_price_reservation(self):
|
||||
"""Test create a reservation, and check price and discounts"""
|
||||
|
||||
def test_general_discount_reservation(self):
|
||||
"""Test a discount in reservation head, and check lines"""
|
||||
10
pms/tests/test_pms_simple_invoice.py
Normal file
10
pms/tests/test_pms_simple_invoice.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
freeze_time("2000-02-02")
|
||||
|
||||
|
||||
class TestPmsInvoiceSimpleInvoice(SavepointCase):
|
||||
def setUp(self):
|
||||
super(TestPmsInvoiceSimpleInvoice, self).setUp()
|
||||
12
pms/views/account_bank_statement_views.xml
Normal file
12
pms/views/account_bank_statement_views.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="bank_statement_form" model="ir.ui.view">
|
||||
<field name="model">account.bank.statement</field>
|
||||
<field name="inherit_id" ref="account.view_bank_statement_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='company_id']" position="after">
|
||||
<field name="property_id" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
12
pms/views/account_journal_views.xml
Normal file
12
pms/views/account_journal_views.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="account_journal_view_form" model="ir.ui.view">
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='company_id']" position="after">
|
||||
<field name="pms_property_ids" widget="many2many_tags" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -6,21 +6,37 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='invoice_date']" position="after">
|
||||
<field name="folio_ids" widget="many2many_tags" />
|
||||
<field name="from_folio" invisible="1" />
|
||||
<field name="from_reservation" invisible="1" />
|
||||
<field name="pms_property_id" invisible="1" />
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//field[@name='invoice_outstanding_credits_debits_widget']"
|
||||
position="before"
|
||||
expr="//field[@name='invoice_line_ids']/tree/field[@name='name']"
|
||||
position="after"
|
||||
>
|
||||
<field
|
||||
name="outstanding_folios_debits_widget"
|
||||
colspan="2"
|
||||
nolabel="1"
|
||||
widget="payment"
|
||||
attrs="{'invisible': [('state', 'not in', 'open')]}"
|
||||
<button
|
||||
name="invoice_filter_days"
|
||||
type="object"
|
||||
icon="fa-calendar"
|
||||
string="Filter-days"
|
||||
aria-label="Change Period"
|
||||
class="float-right"
|
||||
attrs="{
|
||||
'column_invisible': ['|',('parent.from_reservation', '=', False),('parent.state', '!=', 'draft')],
|
||||
'invisible': [('reservation_line_ids', '=', [])]
|
||||
}"
|
||||
/>
|
||||
<field name="reservation_line_ids" invisible="1" />
|
||||
</xpath>
|
||||
<!-- <xpath
|
||||
expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{
|
||||
'readonly': [('reservation_line_ids', '!=', False)],
|
||||
}
|
||||
</attribute>
|
||||
</xpath> -->
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -6,211 +6,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='date']" position="after">
|
||||
<field name="folio_id" />
|
||||
<field name="save_amount" invisible="1" />
|
||||
<field name="save_journal_id" invisible="1" />
|
||||
<field name="save_date" invisible="1" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_payment_view_form_folio" model="ir.ui.view">
|
||||
<field name="name">account.payment.folio.form</field>
|
||||
<field name="model">account.payment</field>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Register Payment" version="7">
|
||||
<header>
|
||||
<button
|
||||
name="action_draft"
|
||||
class="oe_highlight"
|
||||
states="cancelled"
|
||||
string="Set To Draft"
|
||||
type="object"
|
||||
/>
|
||||
<button
|
||||
string="Validate"
|
||||
name="post"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': [('state','!=','draft')]}"
|
||||
/>
|
||||
<button
|
||||
string="Modify"
|
||||
name="modify"
|
||||
type="object"
|
||||
class="oe_edit_only btn-primary"
|
||||
attrs="{'invisible': [('state','=','draft')]}"
|
||||
/>
|
||||
<!-- <button
|
||||
string="Return"
|
||||
name="return_payment_folio"
|
||||
type="object"
|
||||
class="oe_edit_only btn-primary"
|
||||
attrs="{'invisible': [('state','=','draft')]}"
|
||||
/> -->
|
||||
<button
|
||||
string="Delete"
|
||||
name="delete"
|
||||
type="object"
|
||||
class="oe_read_only btn-primary"
|
||||
attrs="{'invisible': [('state','=','draft')]}"
|
||||
/>
|
||||
<button
|
||||
string="Cancel"
|
||||
name="cancel"
|
||||
class="oe_read_only btn-default"
|
||||
special="cancel"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
widget="statusbar"
|
||||
statusbar_visible="draft,posted,reconciled,cancelled"
|
||||
/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<!-- <button
|
||||
class="oe_stat_button"
|
||||
name="button_journal_entries"
|
||||
string="Journal Items"
|
||||
type="object"
|
||||
groups="account.group_account_user"
|
||||
attrs="{'invisible':[('move_line_ids','=',[])]}"
|
||||
icon="fa-bars"
|
||||
/> -->
|
||||
<!-- <field name="move_line_ids" invisible="1" /> -->
|
||||
<!-- <button
|
||||
class="oe_stat_button"
|
||||
name="button_invoices"
|
||||
string="Invoices"
|
||||
type="object"
|
||||
attrs="{'invisible':[('has_invoices','=',False)]}"
|
||||
icon="fa-bars"
|
||||
/> -->
|
||||
<!-- <button
|
||||
class="oe_stat_button"
|
||||
name="open_payment_matching_screen"
|
||||
string="Payment Matching"
|
||||
type="object"
|
||||
attrs="{'invisible':[('move_reconciled','=',True)]}"
|
||||
icon="fa-university"
|
||||
/> -->
|
||||
<!-- <field name="has_invoices" invisible="1" />
|
||||
<field name="move_reconciled" invisible="1" /> -->
|
||||
</div>
|
||||
<field name="id" invisible="1" />
|
||||
<div
|
||||
class="oe_title"
|
||||
attrs="{'invisible': [('state', '=', 'draft')]}"
|
||||
>
|
||||
<h1>
|
||||
<field name="name" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="company_id" invisible="1" />
|
||||
<field name="payment_type" invisible="1" />
|
||||
<field name="save_amount" invisible="1" />
|
||||
<field name="save_date" invisible="1" />
|
||||
<field name="save_journal_id" invisible="1" />
|
||||
<field
|
||||
name="partner_type"
|
||||
widget="selection"
|
||||
invisible="1"
|
||||
/>
|
||||
<field
|
||||
name="partner_id"
|
||||
attrs="{
|
||||
'required': [('state', '=', 'draft'), ('payment_type', 'in', ('inbound', 'outbound'))],
|
||||
'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))]
|
||||
}"
|
||||
context="{
|
||||
'default_is_company': True,
|
||||
'default_supplier': payment_type == 'outbound',
|
||||
'default_customer': payment_type == 'inbound'
|
||||
}"
|
||||
/>
|
||||
<label for="amount" />
|
||||
<div name="amount_div" class="o_row">
|
||||
<field name="amount" />
|
||||
<field
|
||||
name="currency_id"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
groups="base.group_multi_currency"
|
||||
attrs="{'readonly': [('state', '!=', 'draft')]}"
|
||||
/>
|
||||
</div>
|
||||
<field name="journal_id" widget="selection" />
|
||||
<!-- <field
|
||||
name="destination_journal_id"
|
||||
widget="selection"
|
||||
attrs="{
|
||||
'required': [('payment_type', '=', 'transfer')],
|
||||
'invisible': [('payment_type', '!=', 'transfer')],
|
||||
'readonly': [('state', '!=', 'draft')]}"
|
||||
/> -->
|
||||
<field name="hide_payment_method" invisible="1" />
|
||||
<field
|
||||
name="payment_method_id"
|
||||
string=" "
|
||||
widget="radio"
|
||||
attrs="{'invisible': [('hide_payment_method', '=', True)]}"
|
||||
/>
|
||||
<field name="payment_method_code" invisible="1" />
|
||||
<field name="suitable_journal_ids" invisible="1" />
|
||||
<field name="available_payment_method_ids" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<!-- <field name="payment_date" /> -->
|
||||
<!-- <field
|
||||
name="communication"
|
||||
attrs="{'invisible': [('state', '!=', 'draft'), ('communication', '=', False)]}"
|
||||
/> -->
|
||||
<field name="folio_id" readonly="1" force_save="1" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
|
||||
</footer>
|
||||
<div class="oe_chatter">
|
||||
<field
|
||||
name="message_follower_ids"
|
||||
widget="mail_followers"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_payment_view_tree_folio" model="ir.ui.view">
|
||||
<field name="name">account.payment.folio.tree</field>
|
||||
<field name="model">account.payment</field>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-muted="state in ['reconciled', 'cancelled']"
|
||||
edit="false"
|
||||
>
|
||||
<!-- <field name="payment_date" /> -->
|
||||
<field name="name" />
|
||||
<field name="journal_id" />
|
||||
<field name="payment_method_id" />
|
||||
<field name="partner_id" string="Customer" />
|
||||
<field name="amount" sum="Amount" />
|
||||
<field name="state" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="partner_type" invisible="1" />
|
||||
<button
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa fa-2x fa-pencil"
|
||||
name="modify_payment"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
string="Set to Done"
|
||||
help="If a Folio is done, you cannot modify it manually anymore. However, you will still be able to invoice. This is used to freeze the Folio."
|
||||
/>
|
||||
<button
|
||||
name="%(pms.action_view_folio_advance_payment_inv)d"
|
||||
string="Create Invoice"
|
||||
type="action"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': [('invoice_status', '!=', 'to invoice')]}"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
select="2"
|
||||
@@ -35,11 +42,11 @@
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
type="object"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
id="payment_smart_button"
|
||||
icon="fa-money"
|
||||
name="action_pay"
|
||||
name="%(pms.action_payment_folio)d"
|
||||
attrs="{'invisible': [('pending_amount','<=',0)]}"
|
||||
>
|
||||
<div class="o_form_field o_stat_info">
|
||||
@@ -82,7 +89,31 @@
|
||||
widget="percentpie"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
name="action_view_invoice"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-pencil-square-o"
|
||||
attrs="{'invisible': [('invoice_count', '=', 0)]}"
|
||||
>
|
||||
<field
|
||||
name="invoice_count"
|
||||
widget="statinfo"
|
||||
string="Invoices"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Paid"
|
||||
attrs="{'invisible': [('payment_state', '!=', 'paid')]}"
|
||||
/>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Partial"
|
||||
bg_color="bg-warning"
|
||||
attrs="{'invisible': [('payment_state', '!=', 'partial')]}"
|
||||
/>
|
||||
<h2>
|
||||
<field name="name" />
|
||||
</h2>
|
||||
@@ -186,6 +217,7 @@
|
||||
|
||||
</group>
|
||||
<group invisible="1">
|
||||
<field name="payment_state" invisible="1" force_save="1" />
|
||||
<field name="move_ids" invisible="1" />
|
||||
<field name="invoice_status" invisible="1" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
@@ -193,6 +225,116 @@
|
||||
<field name="invoices_paid" invisible="1" />
|
||||
</group>
|
||||
<notebook colspan="4" col="1">
|
||||
<page string="Sale Lines">
|
||||
<field
|
||||
name="sale_line_ids"
|
||||
widget="section_and_note_one2many"
|
||||
>
|
||||
<tree string="Sales Lines" editable="bottom">
|
||||
<control>
|
||||
<create
|
||||
name="add_product_control"
|
||||
string="Add a product"
|
||||
/>
|
||||
<create
|
||||
name="add_section_control"
|
||||
string="Add a section"
|
||||
context="{'default_display_type': 'line_section'}"
|
||||
/>
|
||||
<create
|
||||
name="add_note_control"
|
||||
string="Add a note"
|
||||
context="{'default_display_type': 'line_note'}"
|
||||
/>
|
||||
</control>
|
||||
|
||||
<field name="sequence" widget="handle" />
|
||||
<!-- We do not display the type because we don't want the user to be bothered with that information if he has no section or note. -->
|
||||
<field name="display_type" invisible="1" />
|
||||
|
||||
<!-- <field name="product_updatable" invisible="1"/> -->
|
||||
<field
|
||||
name="product_id"
|
||||
options="{'no_open': True}"
|
||||
force_save="1"
|
||||
/>
|
||||
<field
|
||||
name="name"
|
||||
widget="section_and_note_text"
|
||||
optional="show"
|
||||
/>
|
||||
<field
|
||||
name="analytic_tag_ids"
|
||||
optional="hide"
|
||||
groups="analytic.group_analytic_tags"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color'}"
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
|
||||
/>
|
||||
<field
|
||||
name="product_uom_qty"
|
||||
decoration-info="(not display_type and invoice_status == 'to invoice')"
|
||||
decoration-bf="(not display_type and invoice_status == 'to invoice')"
|
||||
context="{
|
||||
'partner_id': parent.partner_id,
|
||||
'quantity': product_uom_qty,
|
||||
'pricelist': parent.pricelist_id,
|
||||
'uom': product_uom,
|
||||
'company_id': parent.company_id
|
||||
}"
|
||||
/>
|
||||
<field
|
||||
name="qty_invoiced"
|
||||
decoration-info="(not display_type and invoice_status == 'to invoice')"
|
||||
decoration-bf="(not display_type and invoice_status == 'to invoice')"
|
||||
string="Invoiced"
|
||||
attrs="{'column_invisible': [('parent.state', '=', 'draft')]}"
|
||||
optional="show"
|
||||
/>
|
||||
<field name="qty_to_invoice" invisible="1" />
|
||||
<field name="product_uom_readonly" invisible="1" />
|
||||
<field name="reservation_line_ids" invisible="1" />
|
||||
<field
|
||||
name="product_uom"
|
||||
force_save="1"
|
||||
string="UoM"
|
||||
groups="uom.group_uom"
|
||||
options='{"no_open": True}'
|
||||
optional="show"
|
||||
/>
|
||||
<field name="price_unit" />
|
||||
<field
|
||||
name="tax_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create': True}"
|
||||
domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
|
||||
optional="show"
|
||||
/>
|
||||
<field
|
||||
name="discount"
|
||||
string="Disc.%"
|
||||
groups="product.group_discount_per_so_line"
|
||||
optional="show"
|
||||
widget="product_discount"
|
||||
/>
|
||||
<field
|
||||
name="price_subtotal"
|
||||
widget="monetary"
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
/>
|
||||
<field
|
||||
name="price_total"
|
||||
widget="monetary"
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
/>
|
||||
<field name="state" invisible="0" />
|
||||
<field name="invoice_status" invisible="0" />
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="price_tax" invisible="1" />
|
||||
<field name="company_id" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Reservation Rooms">
|
||||
<field
|
||||
name="reservation_ids"
|
||||
@@ -239,24 +381,6 @@
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
<page
|
||||
name="payments"
|
||||
string="Payments"
|
||||
attrs="{'invisible': [('invoices_paid','<=',0)]}"
|
||||
>
|
||||
<field
|
||||
name="payment_ids"
|
||||
context="{'tree_view_ref':'pms.account_payment_view_tree_folio', 'form_view_ref':'pms.account_payment_view_form_folio'}"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
</page>
|
||||
<!-- <page
|
||||
name="returns"
|
||||
string="Retun Payments"
|
||||
attrs="{'invisible': [('refund_amount','<=',0)]}"
|
||||
>
|
||||
<field name="return_ids" options="{'no_create': True}" />
|
||||
</page> -->
|
||||
<page string="Other data" invisible="1">
|
||||
<group>
|
||||
<field name="user_id" />
|
||||
|
||||
@@ -402,6 +402,10 @@
|
||||
<field name="date" readonly="1" force_save="1" />
|
||||
<field name="price" />
|
||||
<field name="discount" />
|
||||
<field
|
||||
name="move_line_ids"
|
||||
widget="many2many_tags"
|
||||
/>
|
||||
<field
|
||||
name="cancel_discount"
|
||||
attrs="{'column_invisible': [('parent.state','!=','cancelled')]}"
|
||||
|
||||
@@ -33,11 +33,6 @@
|
||||
<group>
|
||||
<field name="pms_property_ids" widget="many2many_tags" />
|
||||
<field name="company_id" />
|
||||
<field
|
||||
name="list_price"
|
||||
widget='monetary'
|
||||
options="{'currency_field': 'currency_id', 'field_digits': True}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group colspan="2">
|
||||
@@ -46,6 +41,15 @@
|
||||
<field name="room_ids" widget="many2many_tags" />
|
||||
<field name="total_rooms_count" />
|
||||
</group>
|
||||
<group name="accounting_group">
|
||||
<field
|
||||
name="list_price"
|
||||
widget='monetary'
|
||||
options="{'currency_field': 'currency_id', 'field_digits': True}"
|
||||
/>
|
||||
<field name="taxes_id" widget="many2many_tags" />
|
||||
<field name="categ_id" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Board Services">
|
||||
|
||||
@@ -3,3 +3,6 @@ from . import wizard_massive_changes
|
||||
from . import wizard_advanced_filters
|
||||
from . import wizard_folio
|
||||
from . import wizard_folio_availability
|
||||
from . import folio_make_invoice_advance
|
||||
from . import wizard_invoice_filter_days
|
||||
from . import wizard_payment_folio
|
||||
|
||||
288
pms/wizards/folio_make_invoice_advance.py
Normal file
288
pms/wizards/folio_make_invoice_advance.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FolioAdvancePaymentInv(models.TransientModel):
|
||||
_name = "folio.advance.payment.inv"
|
||||
_description = "Folio Advance Payment Invoice"
|
||||
|
||||
@api.model
|
||||
def _count(self):
|
||||
return len(self._context.get("active_ids", []))
|
||||
|
||||
@api.model
|
||||
def _default_product_id(self):
|
||||
product_id = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("sale.default_deposit_product_id")
|
||||
)
|
||||
return self.env["product.product"].browse(int(product_id)).exists()
|
||||
|
||||
@api.model
|
||||
def _default_deposit_account_id(self):
|
||||
return self._default_product_id()._get_product_accounts()["income"]
|
||||
|
||||
@api.model
|
||||
def _default_deposit_taxes_id(self):
|
||||
return self._default_product_id().taxes_id
|
||||
|
||||
@api.model
|
||||
def _default_has_down_payment(self):
|
||||
if self._context.get("active_model") == "pms.folio" and self._context.get(
|
||||
"active_id", False
|
||||
):
|
||||
folio = self.env["pms.folio"].browse(self._context.get("active_id"))
|
||||
return folio.sale_line_ids.filtered(lambda line: line.is_downpayment)
|
||||
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _default_currency_id(self):
|
||||
if self._context.get("active_model") == "pms.folio" and self._context.get(
|
||||
"active_id", False
|
||||
):
|
||||
sale_order = self.env["pms.folio"].browse(self._context.get("active_id"))
|
||||
return sale_order.currency_id
|
||||
|
||||
advance_payment_method = fields.Selection(
|
||||
[
|
||||
("delivered", "Regular invoice"),
|
||||
("percentage", "Down payment (percentage)"),
|
||||
("fixed", "Down payment (fixed amount)"),
|
||||
],
|
||||
string="Create Invoice",
|
||||
default="delivered",
|
||||
required=True,
|
||||
help="A standard invoice is issued with all the order \
|
||||
lines ready for invoicing, \
|
||||
according to their invoicing policy \
|
||||
(based on ordered or delivered quantity).",
|
||||
)
|
||||
bill_services = fields.Boolean("Bill Services", default=True)
|
||||
bill_rooms = fields.Boolean("Bill Rooms", default=True)
|
||||
deduct_down_payments = fields.Boolean("Deduct down payments", default=True)
|
||||
has_down_payments = fields.Boolean(
|
||||
"Has down payments", default=_default_has_down_payment, readonly=True
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
"product.product",
|
||||
string="Down Payment Product",
|
||||
domain=[("type", "=", "service")],
|
||||
default=_default_product_id,
|
||||
)
|
||||
count = fields.Integer(default=_count, string="Order Count")
|
||||
amount = fields.Float(
|
||||
"Down Payment Amount",
|
||||
digits="Account",
|
||||
help="The percentage of amount to be invoiced in advance, taxes excluded.",
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency", string="Currency", default=_default_currency_id
|
||||
)
|
||||
fixed_amount = fields.Monetary(
|
||||
"Down Payment Amount (Fixed)",
|
||||
help="The fixed amount to be invoiced in advance, taxes excluded.",
|
||||
)
|
||||
deposit_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
string="Income Account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
help="Account used for deposits",
|
||||
default=_default_deposit_account_id,
|
||||
)
|
||||
deposit_taxes_id = fields.Many2many(
|
||||
"account.tax",
|
||||
string="Customer Taxes",
|
||||
help="Taxes used for deposits",
|
||||
default=_default_deposit_taxes_id,
|
||||
)
|
||||
|
||||
@api.onchange("advance_payment_method")
|
||||
def onchange_advance_payment_method(self):
|
||||
if self.advance_payment_method == "percentage":
|
||||
amount = self.default_get(["amount"]).get("amount")
|
||||
return {"value": {"amount": amount}}
|
||||
return {}
|
||||
|
||||
def _prepare_invoice_values(self, order, name, amount, line):
|
||||
invoice_vals = {
|
||||
"ref": order.client_order_ref,
|
||||
"move_type": "out_invoice",
|
||||
"invoice_origin": order.name,
|
||||
"invoice_user_id": order.user_id.id,
|
||||
"narration": order.note,
|
||||
"partner_id": order.partner_invoice_id.id,
|
||||
"currency_id": order.pricelist_id.currency_id.id,
|
||||
"payment_reference": order.reference,
|
||||
"invoice_payment_term_id": order.payment_term_id.id,
|
||||
"partner_bank_id": order.company_id.partner_id.bank_ids[:1].id,
|
||||
# 'campaign_id': order.campaign_id.id,
|
||||
# 'medium_id': order.medium_id.id,
|
||||
# 'source_id': order.source_id.id,
|
||||
"invoice_line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": name,
|
||||
"price_unit": amount,
|
||||
"quantity": 1.0,
|
||||
"product_id": self.product_id.id,
|
||||
"product_uom_id": line.product_uom.id,
|
||||
"tax_ids": [(6, 0, line.tax_ids.ids)],
|
||||
"folio_line_ids": [(6, 0, [line.id])],
|
||||
"analytic_tag_ids": [(6, 0, line.analytic_tag_ids.ids)],
|
||||
"analytic_account_id": order.analytic_account_id.id or False,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
return invoice_vals
|
||||
|
||||
def _get_advance_details(self, order):
|
||||
context = {"lang": order.partner_id.lang}
|
||||
if self.advance_payment_method == "percentage":
|
||||
amount = order.amount_untaxed * self.amount / 100
|
||||
name = _("Down payment of %s%%") % (self.amount)
|
||||
else:
|
||||
amount = self.fixed_amount
|
||||
name = _("Down Payment")
|
||||
del context
|
||||
|
||||
return amount, name
|
||||
|
||||
def _create_invoice(self, order, line, amount):
|
||||
if (self.advance_payment_method == "percentage" and self.amount <= 0.00) or (
|
||||
self.advance_payment_method == "fixed" and self.fixed_amount <= 0.00
|
||||
):
|
||||
raise UserError(_("The value of the down payment amount must be positive."))
|
||||
|
||||
amount, name = self._get_advance_details(order)
|
||||
|
||||
invoice_vals = self._prepare_invoice_values(order, name, amount, line)
|
||||
|
||||
if order.fiscal_position_id:
|
||||
invoice_vals["fiscal_position_id"] = order.fiscal_position_id.id
|
||||
invoice = (
|
||||
self.env["account.move"].sudo().create(invoice_vals).with_user(self.env.uid)
|
||||
)
|
||||
invoice.message_post_with_view(
|
||||
"mail.message_origin_link",
|
||||
values={"self": invoice, "origin": order},
|
||||
subtype_id=self.env.ref("mail.mt_note").id,
|
||||
)
|
||||
return invoice
|
||||
|
||||
def _prepare_line(self, order, analytic_tag_ids, tax_ids, amount):
|
||||
context = {"lang": order.partner_id.lang}
|
||||
so_values = {
|
||||
"name": _("Down Payment: %s") % (time.strftime("%m %Y"),),
|
||||
"price_unit": amount,
|
||||
"product_uom_qty": 0.0,
|
||||
"folio_id": order.id,
|
||||
"discount": 0.0,
|
||||
"product_uom": self.product_id.uom_id.id,
|
||||
"product_id": self.product_id.id,
|
||||
"analytic_tag_ids": analytic_tag_ids,
|
||||
"tax_ids": [(6, 0, tax_ids)],
|
||||
"is_downpayment": True,
|
||||
"sequence": order.sale_line_ids
|
||||
and order.sale_line_ids[-1].sequence + 1
|
||||
or 10,
|
||||
}
|
||||
del context
|
||||
return so_values
|
||||
|
||||
def create_invoices(self):
|
||||
folios = self.env["pms.folio"].browse(self._context.get("active_ids", []))
|
||||
|
||||
if self.advance_payment_method == "delivered":
|
||||
lines_to_invoice = self._get_lines_to_invoice(
|
||||
folios=folios,
|
||||
bill_services=self.bill_services,
|
||||
bill_rooms=self.bill_rooms,
|
||||
)
|
||||
folios._create_invoices(
|
||||
final=self.deduct_down_payments,
|
||||
lines_to_invoice=lines_to_invoice,
|
||||
)
|
||||
else:
|
||||
# Create deposit product if necessary
|
||||
if not self.product_id:
|
||||
vals = self._prepare_deposit_product()
|
||||
self.product_id = self.env["product.product"].create(vals)
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"sale.default_deposit_product_id", self.product_id.id
|
||||
)
|
||||
|
||||
sale_line_obj = self.env["folio.sale.line"]
|
||||
for order in folios:
|
||||
amount, name = self._get_advance_details(order)
|
||||
|
||||
if self.product_id.invoice_policy != "order":
|
||||
raise UserError(
|
||||
_(
|
||||
"""The product used to invoice a down payment should
|
||||
have an invoice policy set to "Ordered quantities".
|
||||
Please update your deposit product to be able
|
||||
to create a deposit invoice."""
|
||||
)
|
||||
)
|
||||
if self.product_id.type != "service":
|
||||
raise UserError(
|
||||
_(
|
||||
"""The product used to invoice a down payment should
|
||||
be of type 'Service'.
|
||||
Please use another product or update this product."""
|
||||
)
|
||||
)
|
||||
taxes = self.product_id.taxes_id.filtered(
|
||||
lambda r: not order.company_id or r.company_id == order.company_id
|
||||
)
|
||||
tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id).ids
|
||||
analytic_tag_ids = []
|
||||
for line in order.sale_line_ids:
|
||||
analytic_tag_ids = [
|
||||
(4, analytic_tag.id, None)
|
||||
for analytic_tag in line.analytic_tag_ids
|
||||
]
|
||||
|
||||
line_values = self._prepare_line(
|
||||
order, analytic_tag_ids, tax_ids, amount
|
||||
)
|
||||
line = sale_line_obj.sudo().create(line_values)
|
||||
self._create_invoice(order, line, amount)
|
||||
if self._context.get("open_invoices", False):
|
||||
return folios.action_view_invoice()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
||||
def _prepare_deposit_product(self):
|
||||
return {
|
||||
"name": "Down payment",
|
||||
"type": "service",
|
||||
"invoice_policy": "order",
|
||||
"property_account_income_id": self.deposit_account_id.id,
|
||||
"taxes_id": [(6, 0, self.deposit_taxes_id.ids)],
|
||||
"company_id": False,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_lines_to_invoice(self, folios, bill_services=True, bill_rooms=True):
|
||||
lines_to_invoice = folios.sale_line_ids
|
||||
if not self.bill_services:
|
||||
lines_to_invoice = lines_to_invoice - lines_to_invoice.filtered(
|
||||
lambda l: l.service_id and not l.service_id.is_board_service
|
||||
)
|
||||
if not self.bill_rooms:
|
||||
lines_to_invoice = lines_to_invoice.filtered(
|
||||
lambda l: l.reservation_id and l.reservation_line_ids
|
||||
)
|
||||
if not lines_to_invoice:
|
||||
raise UserError(_("Nothing to invoice"))
|
||||
return lines_to_invoice
|
||||
127
pms/wizards/folio_make_invoice_advance_views.xml
Normal file
127
pms/wizards/folio_make_invoice_advance_views.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_folio_advance_payment_inv" model="ir.ui.view">
|
||||
<field name="name">Invoice Orders</field>
|
||||
<field name="model">folio.advance.payment.inv</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Invoice Folio Order">
|
||||
<p class="oe_grey">
|
||||
Invoices will be created in draft so that you can review
|
||||
them before validation.
|
||||
</p>
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="count"
|
||||
attrs="{'invisible': [('count','=', 1)]}"
|
||||
readonly="True"
|
||||
/>
|
||||
<field
|
||||
name="advance_payment_method"
|
||||
class="oe_inline"
|
||||
widget="radio"
|
||||
attrs="{'invisible': [('count','>',1)]}"
|
||||
/>
|
||||
<field name="has_down_payments" invisible="1" />
|
||||
<label
|
||||
for="deduct_down_payments"
|
||||
string=""
|
||||
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
|
||||
/>
|
||||
<div
|
||||
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
|
||||
id="down_payment_details"
|
||||
>
|
||||
<field name="deduct_down_payments" nolabel="1" />
|
||||
<label for="deduct_down_payments" />
|
||||
</div>
|
||||
<field
|
||||
name="product_id"
|
||||
context="{'default_invoice_policy': 'order'}"
|
||||
class="oe_inline"
|
||||
invisible="1"
|
||||
/>
|
||||
<label
|
||||
for="amount"
|
||||
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"
|
||||
/>
|
||||
<div
|
||||
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"
|
||||
id="payment_method_details"
|
||||
>
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field
|
||||
name="fixed_amount"
|
||||
attrs="{'required': [('advance_payment_method', '=', 'fixed')], 'invisible': [('advance_payment_method', '!=','fixed')]}"
|
||||
class="oe_inline"
|
||||
/>
|
||||
<field
|
||||
name="amount"
|
||||
attrs="{'required': [('advance_payment_method', '=', 'percentage')], 'invisible': [('advance_payment_method', '!=', 'percentage')]}"
|
||||
class="oe_inline"
|
||||
/>
|
||||
<span
|
||||
attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}"
|
||||
class="oe_inline"
|
||||
>%</span>
|
||||
</div>
|
||||
<field
|
||||
name="deposit_account_id"
|
||||
options="{'no_create': True}"
|
||||
class="oe_inline"
|
||||
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
<field
|
||||
name="deposit_taxes_id"
|
||||
class="oe_inline"
|
||||
widget="many2many_tags"
|
||||
domain="[('type_tax_use','=','sale')]"
|
||||
attrs="{'invisible': ['|', ('advance_payment_method', 'not in', ('fixed', 'percentage')), ('product_id', '!=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="bill_services" widget="boolean_toggle" />
|
||||
<field name="bill_rooms" widget="boolean_toggle" />
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="create_invoices"
|
||||
id="create_invoice_open"
|
||||
string="Create and View Invoice"
|
||||
type="object"
|
||||
context="{'open_invoices': True}"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button
|
||||
name="create_invoices"
|
||||
id="create_invoice"
|
||||
string="Create Invoice"
|
||||
type="object"
|
||||
/>
|
||||
<button
|
||||
string="Cancel"
|
||||
class="btn-secondary"
|
||||
special="cancel"
|
||||
/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record
|
||||
id="action_view_folio_advance_payment_inv"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="name">Create invoices</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">folio.advance.payment.inv</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<!-- TODO: check if we need this -->
|
||||
<field name="binding_model_id" ref="pms.model_pms_folio" />
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
114
pms/wizards/wizard_invoice_filter_days.py
Normal file
114
pms/wizards/wizard_invoice_filter_days.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class InvoiceFilterDays(models.TransientModel):
|
||||
|
||||
_name = "pms.invoice.filter.days"
|
||||
_description = "Filter Days"
|
||||
|
||||
@api.model
|
||||
def default_reservation_lines(self):
|
||||
return (
|
||||
self.env["account.move.line"]
|
||||
.browse(self.env.context.get("active_ids"))
|
||||
.reservation_line_ids
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_move_lines(self):
|
||||
return self.env["account.move.line"].browse(self.env.context.get("active_ids"))
|
||||
|
||||
@api.model
|
||||
def default_from_date(self):
|
||||
return min(
|
||||
self.env["account.move.line"]
|
||||
.browse(self.env.context.get("active_ids"))
|
||||
.reservation_line_ids.mapped("date")
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_to_date(self):
|
||||
return max(
|
||||
self.env["account.move.line"]
|
||||
.browse(self.env.context.get("active_ids"))
|
||||
.reservation_line_ids.mapped("date")
|
||||
)
|
||||
|
||||
move_line_ids = fields.Many2many("account.move.line", default=default_move_lines)
|
||||
move_ids = fields.Many2many("account.move", compute="_compute_move_ids")
|
||||
reservation_line_ids = fields.Many2many(
|
||||
"pms.reservation.line", default=default_reservation_lines
|
||||
)
|
||||
from_date = fields.Date("Date From", default=default_from_date)
|
||||
to_date = fields.Date("Date to", default=default_to_date)
|
||||
date_ids = fields.One2many(
|
||||
comodel_name="pms.invoice.filter.days.items",
|
||||
inverse_name="filter_wizard_id",
|
||||
compute="_compute_date_ids",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
def do_filter(self):
|
||||
self.ensure_one()
|
||||
invoice_lines = self.move_line_ids
|
||||
for line in invoice_lines:
|
||||
reservation_lines = line.reservation_line_ids.filtered(
|
||||
lambda d: d.date in self.date_ids.filtered("included").mapped("date")
|
||||
)
|
||||
if not reservation_lines:
|
||||
raise UserError(_("You can not remove all lines for invoice"))
|
||||
else:
|
||||
# Write on invoice for syncr business/account
|
||||
line.move_id.write(
|
||||
{
|
||||
"invoice_line_ids": [
|
||||
(
|
||||
1,
|
||||
line.id,
|
||||
{
|
||||
"reservation_line_ids": [
|
||||
(6, False, reservation_lines.ids)
|
||||
],
|
||||
"quantity": len(reservation_lines),
|
||||
},
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@api.depends("from_date", "to_date", "reservation_line_ids")
|
||||
def _compute_date_ids(self):
|
||||
self.ensure_one()
|
||||
date_list = [(5, 0, 0)]
|
||||
dates = self.reservation_line_ids.filtered(
|
||||
lambda d: d.date >= self.from_date and d.date <= self.to_date
|
||||
).mapped("date")
|
||||
for date in dates:
|
||||
date_list.append(
|
||||
(
|
||||
0,
|
||||
False,
|
||||
{
|
||||
"date": date,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.date_ids = date_list
|
||||
|
||||
@api.depends("move_line_ids")
|
||||
def _compute_move_ids(self):
|
||||
self.ensure_one()
|
||||
self.move_ids = [(6, 0, self.move_line_ids.mapped("move_id.id"))]
|
||||
|
||||
|
||||
class InvoiceFilterDaysItems(models.TransientModel):
|
||||
|
||||
_name = "pms.invoice.filter.days.items"
|
||||
_description = "Item Days"
|
||||
_rec_name = "date"
|
||||
|
||||
date = fields.Date("Date")
|
||||
included = fields.Boolean("Included", default=True)
|
||||
filter_wizard_id = fields.Many2one(comodel_name="pms.invoice.filter.days")
|
||||
86
pms/wizards/wizard_invoice_filter_days.xml
Normal file
86
pms/wizards/wizard_invoice_filter_days.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="pms_invoice_filter_days_form" model="ir.ui.view">
|
||||
<field name="name">pms.invoice.filter.days.form</field>
|
||||
<field name="model">pms.invoice.filter.days</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div class="o_row">
|
||||
<field
|
||||
name="from_date"
|
||||
widget="daterange"
|
||||
nolabel="1"
|
||||
class="oe_inline"
|
||||
options="{'related_end_date': 'to_date'}"
|
||||
/>
|
||||
<i
|
||||
class="fa fa-long-arrow-right mx-2"
|
||||
aria-label="Arrow icon"
|
||||
title="Arrow"
|
||||
/>
|
||||
<field
|
||||
name="to_date"
|
||||
widget="daterange"
|
||||
nolabel="1"
|
||||
class="oe_inline"
|
||||
options="{'related_start_date': 'from_date'}"
|
||||
/>
|
||||
</div>
|
||||
<group>
|
||||
<field name="move_ids" invisible="0" />
|
||||
<field
|
||||
name="move_line_ids"
|
||||
string="Invoice Lines"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create':True, 'no_open':True}"
|
||||
domain="[
|
||||
('move_id', 'in', move_ids),
|
||||
('reservation_line_ids', '!=', 0),
|
||||
('exclude_from_invoice_tab', '=', False),
|
||||
('display_type', '=', False)
|
||||
]"
|
||||
/>
|
||||
<field
|
||||
name="date_ids"
|
||||
default_focus="1"
|
||||
string="Dates to invoice"
|
||||
>
|
||||
<tree
|
||||
editable="bottom"
|
||||
create="false"
|
||||
delete="false"
|
||||
decoration-muted="not included"
|
||||
decoration-primary="included"
|
||||
>
|
||||
<field name="date" readonly="1" force_save="1" />
|
||||
<field name="included" />
|
||||
</tree>
|
||||
</field>
|
||||
<field name="reservation_line_ids" invisible="1" />
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
string="Apply"
|
||||
name="do_filter"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button
|
||||
string="Cancel"
|
||||
class="btn btn-secondary"
|
||||
special="cancel"
|
||||
/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="pms_invoice_filter_days_action" model="ir.actions.act_window">
|
||||
<field name="name">Filter Days</field>
|
||||
<field name="res_model">pms.invoice.filter.days</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
124
pms/wizards/wizard_payment_folio.py
Normal file
124
pms/wizards/wizard_payment_folio.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WizardPaymentFolio(models.TransientModel):
|
||||
|
||||
_name = "wizard.payment.folio"
|
||||
_description = "Payments"
|
||||
|
||||
@api.model
|
||||
def default_folio_id(self):
|
||||
return self.env["pms.folio"].browse(self._context.get("active_id", [])).id
|
||||
|
||||
@api.model
|
||||
def _default_amount(self):
|
||||
folio = self.env["pms.folio"].browse(self._context.get("active_id", []))
|
||||
return folio.pending_amount
|
||||
|
||||
@api.model
|
||||
def _default_partner(self):
|
||||
folio = self.env["pms.folio"].browse(self._context.get("active_id", []))
|
||||
return folio.partner_id.id
|
||||
|
||||
folio_id = fields.Many2one(
|
||||
"pms.folio",
|
||||
string="Folio",
|
||||
required=True,
|
||||
default=default_folio_id,
|
||||
)
|
||||
reservation_ids = fields.Many2many(
|
||||
"pms.reservation",
|
||||
string="Reservations",
|
||||
)
|
||||
service_ids = fields.Many2many(
|
||||
"pms.service",
|
||||
string="Services",
|
||||
)
|
||||
payment_method_id = fields.Many2one(
|
||||
"account.journal",
|
||||
string="Payment Method",
|
||||
required=True,
|
||||
domain="[('id', 'in', allowed_method_ids)]",
|
||||
)
|
||||
allowed_method_ids = fields.Many2many(
|
||||
"account.journal",
|
||||
"allowed_payment_journal_rel",
|
||||
"payment_id",
|
||||
"journal_id",
|
||||
compute="_compute_allowed_method_ids",
|
||||
store="True",
|
||||
)
|
||||
amount = fields.Float("Amount", digits=("Product Price"), default=_default_amount)
|
||||
date = fields.Date("Date", default=fields.Date.context_today, required=True)
|
||||
partner_id = fields.Many2one("res.partner", default=_default_partner)
|
||||
|
||||
@api.depends("folio_id")
|
||||
def _compute_allowed_method_ids(self):
|
||||
self.ensure_one()
|
||||
journal_ids = False
|
||||
if self.folio_id:
|
||||
journal_ids = self.folio_id.pms_property_id._get_payment_methods().ids
|
||||
self.allowed_method_ids = journal_ids
|
||||
|
||||
def button_payment(self):
|
||||
BankStatementLine = self.env["account.bank.statement.line"]
|
||||
line = self._get_statement_line_vals(
|
||||
journal=self.payment_method_id,
|
||||
receivable_account=self.payment_method_id.suspense_account_id,
|
||||
user=self.env.user,
|
||||
amount=self.amount,
|
||||
folios=self.folio_id,
|
||||
partner=self.partner_id,
|
||||
date=self.date,
|
||||
)
|
||||
BankStatementLine.sudo().create(line)
|
||||
|
||||
def _get_statement_line_vals(
|
||||
self,
|
||||
journal,
|
||||
receivable_account,
|
||||
user,
|
||||
amount,
|
||||
folios,
|
||||
reservations=False,
|
||||
services=False,
|
||||
partner=False,
|
||||
date=False,
|
||||
):
|
||||
property_folio_id = folios.mapped("pms_property_id.id")
|
||||
if len(property_folio_id) != 1:
|
||||
raise ValidationError(_("Only can payment by property"))
|
||||
statement = (
|
||||
self.env["account.bank.statement"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("journal_id", "=", journal.id),
|
||||
("property_id", "=", property_folio_id[0]),
|
||||
("state", "=", "open"),
|
||||
]
|
||||
)
|
||||
)
|
||||
reservation_ids = reservations.ids if reservations else []
|
||||
service_ids = services.ids if services else []
|
||||
# TODO: If not open statement, create new, with cash control option
|
||||
if statement:
|
||||
return {
|
||||
"date": date,
|
||||
"amount": amount,
|
||||
"partner_id": partner.id if partner else False,
|
||||
"statement_folio_ids": [(6, 0, folios.ids)],
|
||||
"reservation_ids": [(6, 0, reservation_ids)],
|
||||
"service_ids": [(6, 0, service_ids)],
|
||||
"payment_ref": folios.mapped("name"),
|
||||
"statement_id": statement.id,
|
||||
"journal_id": statement.journal_id.id,
|
||||
"counterpart_account_id": receivable_account.id,
|
||||
}
|
||||
else:
|
||||
return False
|
||||
43
pms/wizards/wizard_payment_folio.xml
Normal file
43
pms/wizards/wizard_payment_folio.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="wizard_payment_folio_view_form" model="ir.ui.view">
|
||||
<field name="name">wizard.payment.folio.view.form</field>
|
||||
<field name="model">wizard.payment.folio</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Payment">
|
||||
<group>
|
||||
<group>
|
||||
<field name="allowed_method_ids" invisible="1" />
|
||||
<field name="payment_method_id" widget="radio" />
|
||||
<field name="amount" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="date" />
|
||||
<field name="partner_id" />
|
||||
<field name="folio_id" />
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
id="payment"
|
||||
name="button_payment"
|
||||
string="Pay"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_payment_folio" model="ir.actions.act_window">
|
||||
<field name="name">Payment Folio</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">wizard.payment.folio</field>
|
||||
<field name="view_id" ref="wizard_payment_folio_view_form" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user