split_pendapatan_payment/models/pos_session.py

265 lines
14 KiB
Python

# -*- coding: utf-8 -*-
from collections import defaultdict
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero, float_compare
class PosSession(models.Model):
_inherit = 'pos.session'
def _prepare_line(self, order_line):
"""Override to allow products without income accounts when payment methods have them configured"""
def get_income_account(order_line):
product = order_line.product_id
income_account = product.with_company(order_line.company_id)._get_product_accounts()['income'] or self.config_id.journal_id.default_account_id
# NEW: If no income account is found on the product, check if payment methods have income accounts configured
if not income_account:
# First look at payment methods actually used on the order
payment_methods_used = order_line.order_id.payment_ids.mapped('payment_method_id').filtered('income_account_id')
payment_methods_with_income = payment_methods_used or self.payment_method_ids.filtered('income_account_id')
payment_income_account = payment_methods_with_income[:1].income_account_id
if payment_income_account:
income_account = payment_income_account
else:
raise UserError(_('Please define income account for this product: "%s" (id:%d).\n'
'Or configure the Income Account in your payment methods.',
product.name, product.id))
return order_line.order_id.fiscal_position_id.map_account(income_account)
company_domain = self.env['account.tax']._check_company_domain(order_line.order_id.company_id)
tax_ids = order_line.tax_ids_after_fiscal_position.filtered_domain(company_domain)
sign = -1 if order_line.qty >= 0 else 1
price = sign * order_line.price_unit * (1 - (order_line.discount or 0.0) / 100.0)
check_refund = lambda x: x.qty * x.price_unit < 0
is_refund = check_refund(order_line)
tax_data = tax_ids.compute_all(price_unit=price, quantity=abs(order_line.qty), currency=self.currency_id, is_refund=is_refund, fixed_multiplicator=sign, include_caba_tags=True)
date_order = order_line.order_id.date_order
taxes = [{'date_order': date_order, **tax} for tax in tax_data['taxes']]
return {
'date_order': order_line.order_id.date_order,
'income_account_id': get_income_account(order_line).id,
'amount': order_line.price_subtotal,
'taxes': taxes,
'base_tags': tuple(tax_data['base_tags']),
}
def _accumulate_amounts(self, data):
"""Distribute sales amounts per payment method while respecting discount accounts."""
data = super(PosSession, self)._accumulate_amounts(data)
closed_orders = self._get_closed_orders()
if not closed_orders:
return data
sales = data.get('sales', {})
if not sales:
return data
discount_product_id = self.config_id.discount_product_id.id if self.config_id.discount_product_id else None
# Build per-order breakdown of sales keyed by (account, sign, tax tuple, tags)
order_payment_totals = {}
order_sales_breakdown = {}
for order in closed_orders:
order_payment_totals[order.id] = sum(payment.amount for payment in order.payment_ids)
if order.is_invoiced:
continue
sale_map = {}
for order_line in order.lines:
line_vals = self._prepare_line(order_line)
sale_key = (
line_vals['income_account_id'],
-1 if line_vals['amount'] < 0 else 1,
tuple((tax['id'], tax['account_id'], tax['tax_repartition_line_id']) for tax in line_vals['taxes']),
line_vals['base_tags'],
)
entry = sale_map.setdefault(sale_key, {
'amount': 0.0,
'amount_converted': 0.0,
'tax_amount': 0.0,
'regular_amount': 0.0,
'regular_amount_converted': 0.0,
'regular_tax_amount': 0.0,
'discount_amount': 0.0,
'discount_amount_converted': 0.0,
'discount_tax_amount': 0.0,
})
prev_amount = entry['amount']
prev_amount_converted = entry['amount_converted']
updated_amounts = self._update_amounts(
{'amount': prev_amount, 'amount_converted': prev_amount_converted},
{'amount': line_vals['amount']},
order.date_order,
round=False,
)
line_amount_converted = updated_amounts['amount_converted'] - prev_amount_converted
line_tax_amount = sum(tax['amount'] for tax in line_vals['taxes'])
entry['amount'] = updated_amounts['amount']
entry['amount_converted'] = updated_amounts['amount_converted']
entry['tax_amount'] += line_tax_amount
is_reward_line = bool(getattr(order_line, 'is_reward_line', False))
has_reward = bool(getattr(order_line, 'reward_id', False))
is_discount_product = bool(discount_product_id and order_line.product_id.id == discount_product_id)
is_discount_line = is_reward_line or has_reward or is_discount_product
if is_discount_line:
entry['discount_amount'] += line_vals['amount']
entry['discount_amount_converted'] += line_amount_converted
entry['discount_tax_amount'] += line_tax_amount
else:
entry['regular_amount'] += line_vals['amount']
entry['regular_amount_converted'] += line_amount_converted
entry['regular_tax_amount'] += line_tax_amount
if sale_map:
order_sales_breakdown[order.id] = sale_map
split_sales = defaultdict(lambda: {'amount': 0.0, 'amount_converted': 0.0, 'tax_amount': 0.0})
for sale_key, sale_amounts in sales.items():
if len(sale_key) < 4:
continue
total_amount = sale_amounts['amount']
if float_is_zero(total_amount, precision_rounding=self.currency_id.rounding):
continue
for order in closed_orders:
if order.is_invoiced:
continue
order_sale_map = order_sales_breakdown.get(order.id)
if not order_sale_map:
continue
order_sale_amounts = order_sale_map.get(sale_key)
if not order_sale_amounts:
continue
order_payment_total = order_payment_totals.get(order.id, 0.0)
#if float_is_zero(order_payment_total, precision_rounding=order.currency_id.rounding):
# continue
order_amount = order_sale_amounts['amount']
order_amount_converted = order_sale_amounts['amount_converted']
order_tax_amount = order_sale_amounts['tax_amount']
if float_is_zero(order_amount, precision_rounding=self.currency_id.rounding):
continue
for payment in order.payment_ids:
#if float_is_zero(payment.amount, precision_rounding=order.currency_id.rounding):
# continue
if float_is_zero(payment.amount, precision_rounding=order.currency_id.rounding):
payment_proportion = 1.0
else:
payment_proportion = payment.amount / order_payment_total
net_amount = order_amount * payment_proportion
net_amount_converted = order_amount_converted * payment_proportion
net_tax_amount = order_tax_amount * payment_proportion
regular_part_amount = order_sale_amounts['regular_amount'] * payment_proportion
discount_part_amount = order_sale_amounts['discount_amount'] * payment_proportion
regular_part_amount_converted = order_sale_amounts['regular_amount_converted'] * payment_proportion
discount_part_amount_converted = order_sale_amounts['discount_amount_converted'] * payment_proportion
regular_part_tax = order_sale_amounts['regular_tax_amount'] * payment_proportion
discount_part_tax = order_sale_amounts['discount_tax_amount'] * payment_proportion
residual_amount = net_amount - (regular_part_amount + discount_part_amount)
residual_amount_converted = net_amount_converted - (regular_part_amount_converted + discount_part_amount_converted)
residual_tax = net_tax_amount - (regular_part_tax + discount_part_tax)
if abs(regular_part_amount) >= abs(discount_part_amount):
regular_part_amount += residual_amount
regular_part_amount_converted += residual_amount_converted
regular_part_tax += residual_tax
else:
discount_part_amount += residual_amount
discount_part_amount_converted += residual_amount_converted
discount_part_tax += residual_tax
def add_split_entry(part_amount, part_amount_converted, part_tax_amount, is_discount_part):
if float_is_zero(part_amount, precision_rounding=self.currency_id.rounding):
return
target_account_id = sale_key[0]
if is_discount_part:
if payment.payment_method_id.discount_account_id:
target_account_id = payment.payment_method_id.discount_account_id.id
elif payment.payment_method_id.income_account_id:
target_account_id = payment.payment_method_id.income_account_id.id
else:
if payment.payment_method_id.income_account_id:
target_account_id = payment.payment_method_id.income_account_id.id
if not target_account_id:
raise UserError(_(
'No income account found for payment method "%s".\n'
'Please configure the Income Account in the payment method settings, '
'or ensure the product has a valid income account defined.'
) % payment.payment_method_id.name)
new_sale_key = (
target_account_id,
sale_key[1],
payment.payment_method_id.id,
sale_key[2],
sale_key[3],
)
split_sales[new_sale_key]['amount'] += part_amount
split_sales[new_sale_key]['amount_converted'] += part_amount_converted
split_sales[new_sale_key]['tax_amount'] += part_tax_amount
add_split_entry(regular_part_amount, regular_part_amount_converted, regular_part_tax, False)
add_split_entry(discount_part_amount, discount_part_amount_converted, discount_part_tax, True)
data['sales'] = split_sales
return data
def _get_sale_vals(self, key, amount, amount_converted):
""" Override to add payment method information to the sales line description """
# Check if this key includes payment method information
if len(key) >= 5 and isinstance(key[2], int): # Has payment method ID
account_id, sign, payment_method_id, tax_keys, base_tag_ids = key
# Try to get the payment method name
try:
payment_method = self.env['pos.payment.method'].browse(payment_method_id)
payment_method_name = payment_method.name
except:
payment_method_name = "Unknown Payment"
else:
# Original format
account_id, sign, tax_keys, base_tag_ids = key
payment_method_name = None
tax_ids = set(tax[0] for tax in tax_keys) if tax_keys else set()
applied_taxes = self.env['account.tax'].browse(tax_ids)
title = _('Sales') if sign == 1 else _('Refund')
# Create name with payment method information
if payment_method_name:
name = _('%s - %s', title, payment_method_name)
if applied_taxes:
name = _('%s with %s - %s', title, ', '.join([tax.name for tax in applied_taxes]), payment_method_name)
else:
name = _('%s untaxed', title)
if applied_taxes:
name = _('%s with %s', title, ', '.join([tax.name for tax in applied_taxes]))
partial_vals = {
'name': name,
'account_id': account_id,
'move_id': self.move_id.id,
'tax_ids': [(6, 0, tax_ids)],
'tax_tag_ids': [(6, 0, base_tag_ids)] if base_tag_ids else [],
}
return self._credit_amounts(partial_vals, amount, amount_converted)