# -*- 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)