You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

262 lines
12 KiB

# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Anfas Faisal K (odoo@cybrosys.info)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from odoo import api, models, fields, _
from odoo.exceptions import UserError
from odoo.osv.expression import AND, _logger
from odoo.tools.float_utils import float_round, float_is_zero, float_compare
try:
# Optional: use psycopg2 constants if available for more robust checks
from psycopg2 import errors as pg_errors
except Exception:
pg_errors = None
class PosOrder(models.Model):
"""
This class extends the 'pos.order' model to introduce additional
functionality related to partial payments and order management in the
Point of Sale (POS) system.
It adds fields and methods for tracking partial payments, computing due
amounts, and marking orders as paid. The class also includes a method to
search for partial orders based on specified criteria.
"""
_inherit = 'pos.order'
is_partial_payment = fields.Boolean(string="Is Partial Payment",
default=False,
help="Flag indicating whether this POS "
"order is a partial payment.")
due_amount = fields.Float(string="Amount Due",
compute='_compute_due_amount',
store=True,
help="The amount remaining to be paid for this"
"POS order.")
@api.depends('amount_total', 'amount_paid')
def _compute_due_amount(self):
"""
Compute the due amount for the POS order.
This method computes the difference between the total amount and the amount paid
for the POS order and updates the 'due_amount' field accordingly.
"""
for record in self:
record.due_amount = record.amount_total - record.amount_paid
def _order_fields(self, ui_order):
"""
Prepare dictionary for create method
This method prepares a dictionary of order fields for creating a POS order based
on the data from the user interface (UI) order.
"""
result = super()._order_fields(ui_order)
result['is_partial_payment'] = ui_order.get('is_partial_payment')
return result
def action_pos_order_paid(self):
"""
Mark the POS order as paid. This method marks the POS order as
paid and ensures that it is fully paid based on the partial
payment.
"""
self.ensure_one()
# compute the total to compare taking into account cash rounding config on the order's config
if (not self.config_id.cash_rounding) or (
self.config_id.only_round_cash_method and not any(
p.payment_method_id.is_cash_count for p in self.payment_ids)):
total = self.amount_total
else:
# use the configured rounding method value and rounding algorithm if present
precision_rounding = getattr(self.config_id.rounding_method,
'rounding',
self.currency_id.rounding) or self.currency_id.rounding
total = float_round(self.amount_total,
precision_rounding=precision_rounding,
rounding_method=getattr(
self.config_id.rounding_method,
'rounding_method', None))
isPaid = float_is_zero(total - self.amount_paid,
precision_rounding=self.currency_id.rounding)
# Only allow marking as paid for partial payments when this order specifically is flagged as partial payment
if not isPaid:
# previously code flipped isPaid True if any pos_config.partial_payment existed,
# which caused incorrect marking. Instead: only allow if the *current order* is a partial payment order.
if self.is_partial_payment:
isPaid = True
# Keep the cash rounding tolerance check if still not isPaid (original behavior)
if not isPaid and not self.config_id.cash_rounding:
raise UserError(_("Order %s is not fully paid.") % (self.name,))
elif not isPaid and self.config_id.cash_rounding:
currency = self.currency_id
if self.config_id.rounding_method.rounding_method == "HALF-UP":
maxDiff = currency.round(
self.config_id.rounding_method.rounding / 2)
else:
maxDiff = currency.round(
self.config_id.rounding_method.rounding)
diff = currency.round(self.amount_total - self.amount_paid)
if not abs(diff) <= maxDiff:
raise UserError(_("Order %s is not fully paid.") % (self.name,))
self.write({'state': 'paid'})
return True
@api.model
def search_partial_order_ids(self, config_id, domain, limit, offset):
"""Search for 'partial' orders that satisfy the given domain,
limit and offset."""
default_domain = ['&', ('config_id', '=', config_id),
('is_partial_payment', '=', True), '!', '|',
('state', '=', 'draft'), ('state', '=', 'cancelled')]
real_domain = AND([domain, default_domain])
ids = self.search(AND([domain, default_domain]), limit=limit,
offset=offset).ids
totalCount = self.search_count(real_domain)
return {'ids': ids, 'totalCount': totalCount}
def _create_invoice(self, move_vals):
"""
Create the account.move (invoice) and apply cash rounding adjustments
ONLY if the order is effectively paid within rounding tolerance.
NOTE: do NOT call super()._create_invoice(move_vals) because the original
implementation already contains rounding logic that would run unconditionally.
"""
self.ensure_one()
# Create the invoice move directly (same as original POS code)
new_move = self.env['account.move'].sudo().with_company(
self.company_id).with_context(
default_move_type=move_vals['move_type']
).create(move_vals)
# Post a message on the invoice linking to the POS order
message = _(
"This invoice has been created from the point of sale session: %s",
self._get_html_link(),
)
new_move.message_post(body=message)
# If no cash rounding configured for the POS, nothing to do
if not self.config_id.cash_rounding:
return new_move
# determine rounding precision
rounding_precision = new_move.currency_id.rounding or self.currency_id.rounding or 0.0
# Determine whether the order is paid within rounding tolerance
is_paid_within_rounding = float_compare(self.amount_paid,
self.amount_total,
precision_rounding=rounding_precision) >= 0
# If only_round_cash_method is set, require at least one cash payment
if self.config_id.only_round_cash_method and not any(
p.payment_method_id.is_cash_count for p in self.payment_ids):
apply_rounding = False
else:
apply_rounding = is_paid_within_rounding
# If not applying rounding, return the invoice as-is
if not apply_rounding:
return new_move
# Compute rounding applied and adjust rounding + receivable lines
rounding_applied = float_round(self.amount_paid - self.amount_total,
precision_rounding=rounding_precision)
rounding_line_difference = 0.0
rounding_line = new_move.line_ids.filtered(
lambda line: line.display_type == 'rounding')
if rounding_line and rounding_line.filtered(lambda l: l.debit > 0):
rl = rounding_line.filtered(lambda l: l.debit > 0)[0]
rounding_line_difference = rl.debit + rounding_applied
elif rounding_line and rounding_line.filtered(lambda l: l.credit > 0):
rl = rounding_line.filtered(lambda l: l.credit > 0)[0]
rounding_line_difference = -rl.credit + rounding_applied
else:
rounding_line_difference = rounding_applied
if rounding_applied:
if rounding_applied > 0.0:
account_id = new_move.invoice_cash_rounding_id.loss_account_id.id
else:
account_id = new_move.invoice_cash_rounding_id.profit_account_id.id
if rounding_line:
if rounding_line_difference:
rounding_line.with_context(skip_invoice_sync=True,
check_move_validity=False).write(
{
'debit': rounding_applied < 0.0 and -rounding_applied or 0.0,
'credit': rounding_applied > 0.0 and rounding_applied or 0.0,
'account_id': account_id,
'price_unit': rounding_applied,
})
else:
# create the rounding line
self.env['account.move.line'].with_context(
skip_invoice_sync=True, check_move_validity=False).create({
'balance': -rounding_applied,
'quantity': 1.0,
'partner_id': new_move.partner_id.id,
'move_id': new_move.id,
'currency_id': new_move.currency_id.id,
'company_id': new_move.company_id.id,
'company_currency_id': new_move.company_id.currency_id.id,
'display_type': 'rounding',
'sequence': 9999,
'name': self.config_id.rounding_method.name,
'account_id': account_id,
})
else:
if rounding_line:
rounding_line.with_context(skip_invoice_sync=True,
check_move_validity=False).unlink()
if rounding_line_difference:
existing_terms_line = new_move.line_ids.filtered(
lambda line: line.account_id.account_type in (
'asset_receivable', 'liability_payable'))
if existing_terms_line:
if existing_terms_line.debit > 0:
existing_terms_line_new_val = float_round(
existing_terms_line.debit + rounding_line_difference,
precision_rounding=rounding_precision)
else:
existing_terms_line_new_val = float_round(
-existing_terms_line.credit + rounding_line_difference,
precision_rounding=rounding_precision)
existing_terms_line.with_context(skip_invoice_sync=True).write({
'debit': existing_terms_line_new_val > 0.0 and existing_terms_line_new_val or 0.0,
'credit': existing_terms_line_new_val < 0.0 and -existing_terms_line_new_val or 0.0,
})
return new_move