From 6d7a8a994a1b31b2e25e7ec37c7ad8e6c8a552bf Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Thu, 28 Apr 2016 16:21:25 +0200 Subject: [PATCH] [FIX] Honour mapped taxes when creating move lines --- pos_pricelist/README.rst | 1 + pos_pricelist/models/__init__.py | 1 + pos_pricelist/models/point_of_sale.py | 11 +- pos_pricelist/models/pos_order_patch.py | 233 ++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 pos_pricelist/models/pos_order_patch.py diff --git a/pos_pricelist/README.rst b/pos_pricelist/README.rst index 16ef83b8..bb4204f2 100644 --- a/pos_pricelist/README.rst +++ b/pos_pricelist/README.rst @@ -86,6 +86,7 @@ Missing features ---------------- * As you may know, product template is not fully implemented in the POS, so I decided to drop it from this module. +* Applying a fiscal position on a product with inclusive taxes is not yet supported. In this case, the mapped taxes will be applied to the price incuding taxes. Bug Tracker diff --git a/pos_pricelist/models/__init__.py b/pos_pricelist/models/__init__.py index 9d729a0a..467e1253 100644 --- a/pos_pricelist/models/__init__.py +++ b/pos_pricelist/models/__init__.py @@ -6,3 +6,4 @@ from . import account_fiscal_position from . import pos_pricelist from . import point_of_sale +from . import pos_order_patch diff --git a/pos_pricelist/models/point_of_sale.py b/pos_pricelist/models/point_of_sale.py index 1b0e1d83..9291f19d 100644 --- a/pos_pricelist/models/point_of_sale.py +++ b/pos_pricelist/models/point_of_sale.py @@ -18,9 +18,12 @@ # ############################################################################## - from openerp import models, fields, api from openerp.addons import decimal_precision as dp +from openerp.addons.point_of_sale.point_of_sale import pos_order as base_order +from openerp.addons.pos_pricelist.models.pos_order_patch import ( + _create_account_move_line) + import logging _logger = logging.getLogger(__name__) @@ -158,3 +161,9 @@ class PosOrder(models.Model): # Compute tax detail orders.compute_tax_detail() _logger.info("%d orders computed installing module.", len(orders)) + + + def _register_hook(self, cr): + res = super(PosOrder, self)._register_hook(cr) + base_order._create_account_move_line = _create_account_move_line + return res diff --git a/pos_pricelist/models/pos_order_patch.py b/pos_pricelist/models/pos_order_patch.py new file mode 100644 index 00000000..ae655099 --- /dev/null +++ b/pos_pricelist/models/pos_order_patch.py @@ -0,0 +1,233 @@ +# coding: utf-8 +# Copyright: Odoo S.A. +# License: AGPL-3 +# flake8: noqa +from openerp.tools.translate import _ + + +def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None): + """ Monkeypatch for this method's version on pos.order in the point_of_sale + module. Only change is to refer to the line's taxes instead of the + product's taxes (change below is marked with 'pos_pricelist'). Keep in a + separate file so that it can be excluded from flake8 inspection. """ + if True: # Keep indentation level for reference purposes + # Tricky, via the workflow, we only have one id in the ids variable + """Create a account move line of order grouped by products or not.""" + account_move_obj = self.pool.get('account.move') + account_period_obj = self.pool.get('account.period') + account_tax_obj = self.pool.get('account.tax') + property_obj = self.pool.get('ir.property') + cur_obj = self.pool.get('res.currency') + + #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) + + if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): + raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!')) + + grouped_data = {} + have_to_group_by = session and session.config_id.group_by or False + + def compute_tax(amount, tax, line): + if amount > 0: + tax_code_id = tax['base_code_id'] + tax_amount = line.price_subtotal * tax['base_sign'] + else: + tax_code_id = tax['ref_base_code_id'] + tax_amount = abs(line.price_subtotal) * tax['ref_base_sign'] + + return (tax_code_id, tax_amount,) + + for order in self.browse(cr, uid, ids, context=context): + if order.account_move: + continue + if order.state != 'paid': + continue + + current_company = order.sale_journal.company_id + + group_tax = {} + account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context) + + order_account = order.partner_id and \ + order.partner_id.property_account_receivable and \ + order.partner_id.property_account_receivable.id or \ + account_def and account_def.id + + if move_id is None: + # Create an entry for the sale + move_id = self._create_account_move(cr, uid, order.session_id.start_at, order.name, order.sale_journal.id, order.company_id.id, context=context) + + move = account_move_obj.browse(cr, uid, move_id, context=context) + + def insert_data(data_type, values): + # if have_to_group_by: + + sale_journal_id = order.sale_journal.id + + # 'quantity': line.qty, + # 'product_id': line.product_id.id, + values.update({ + 'date': order.date_order[:10], + 'ref': order.name, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False, + 'journal_id' : sale_journal_id, + 'period_id': move.period_id.id, + 'move_id' : move_id, + 'company_id': current_company.id, + }) + + if data_type == 'product': + key = ('product', values['partner_id'], values['product_id'], values['analytic_account_id'], values['debit'] > 0) + elif data_type == 'tax': + key = ('tax', values['partner_id'], values['tax_code_id'], values['debit'] > 0) + elif data_type == 'counter_part': + key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0) + else: + return + + grouped_data.setdefault(key, []) + + # if not have_to_group_by or (not grouped_data[key]): + # grouped_data[key].append(values) + # else: + # pass + + if have_to_group_by: + if not grouped_data[key]: + grouped_data[key].append(values) + else: + for line in grouped_data[key]: + if line.get('tax_code_id') == values.get('tax_code_id'): + current_value = line + current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) + current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) + current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + break + else: + grouped_data[key].append(values) + else: + grouped_data[key].append(values) + + #because of the weird way the pos order is written, we need to make sure there is at least one line, + #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that + #are set inside the for loop) + #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack + assert order.lines, _('The POS order must have lines when calling this method') + # Create an move for each order line + + cur = order.pricelist_id.currency_id + round_per_line = True + if order.company_id.tax_calculation_rounding_method == 'round_globally': + round_per_line = False + for line in order.lines: + tax_amount = 0 + taxes = [] + # [pos_pricelist] Only change in the next line: + # for t in line.product_id.taxes_id: + for t in line.tax_ids if 'tax_ids' in line._fields else line.product_id.taxes_id: + if t.company_id.id == current_company.id: + taxes.append(t) + computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes'] + + for tax in computed_taxes: + tax_amount += cur_obj.round(cr, uid, cur, tax['amount']) if round_per_line else tax['amount'] + if tax_amount < 0: + group_key = (tax['ref_tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id']) + else: + group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id']) + + group_tax.setdefault(group_key, 0) + group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount']) if round_per_line else tax['amount'] + + amount = line.price_subtotal + + # Search for the income account + if line.product_id.property_account_income.id: + income_account = line.product_id.property_account_income.id + elif line.product_id.categ_id.property_account_income_categ.id: + income_account = line.product_id.categ_id.property_account_income_categ.id + else: + raise osv.except_osv(_('Error!'), _('Please define income '\ + 'account for this product: "%s" (id:%d).') \ + % (line.product_id.name, line.product_id.id, )) + + # Empty the tax list as long as there is no tax code: + tax_code_id = False + tax_amount = 0 + while computed_taxes: + tax = computed_taxes.pop(0) + tax_code_id, tax_amount = compute_tax(amount, tax, line) + + # If there is one we stop + if tax_code_id: + break + + # Create a move for the line + insert_data('product', { + 'name': line.product_id.name, + 'quantity': line.qty, + 'product_id': line.product_id.id, + 'account_id': income_account, + 'analytic_account_id': self._prepare_analytic_account(cr, uid, line, context=context), + 'credit': ((amount>0) and amount) or 0.0, + 'debit': ((amount<0) and -amount) or 0.0, + 'tax_code_id': tax_code_id, + 'tax_amount': tax_amount, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + }) + + # For each remaining tax with a code, whe create a move line + for tax in computed_taxes: + tax_code_id, tax_amount = compute_tax(amount, tax, line) + if not tax_code_id: + continue + + insert_data('tax', { + 'name': _('Tax'), + 'product_id':line.product_id.id, + 'quantity': line.qty, + 'account_id': income_account, + 'credit': 0.0, + 'debit': 0.0, + 'tax_code_id': tax_code_id, + 'tax_amount': tax_amount, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + }) + + # Create a move for each tax group + (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3) + + for key, tax_amount in group_tax.items(): + tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context) + insert_data('tax', { + 'name': _('Tax') + ' ' + tax.name, + 'quantity': line.qty, + 'product_id': line.product_id.id, + 'account_id': key[account_pos] or income_account, + 'credit': ((tax_amount>0) and tax_amount) or 0.0, + 'debit': ((tax_amount<0) and -tax_amount) or 0.0, + 'tax_code_id': key[tax_code_pos], + 'tax_amount': abs(tax_amount) * tax.tax_sign if tax_amount>=0 else abs(tax_amount) * tax.ref_tax_sign, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + }) + + # counterpart + insert_data('counter_part', { + 'name': _("Trade Receivables"), #order.name, + 'account_id': order_account, + 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, + 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False + }) + + order.write({'state':'done', 'account_move': move_id}) + + all_lines = [] + for group_key, group_data in grouped_data.iteritems(): + for value in group_data: + all_lines.append((0, 0, value),) + if move_id: #In case no order was changed + self.pool.get("account.move").write(cr, uid, [move_id], {'line_id':all_lines}, context=context) + + return True