265 lines
14 KiB
Python
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)
|