diff --git a/pos_pricelist/README.rst b/pos_pricelist/README.rst index 16ef83b8..6a4f69f3 100644 --- a/pos_pricelist/README.rst +++ b/pos_pricelist/README.rst @@ -52,9 +52,9 @@ The POS will recognize it and will compute the price according to the rule defin - Implemented Rules are : -1. (-1) : Rule based on other pricelist -2. (-2) : Rule based on supplierinfo -3. (default) : Any price type which is set on the product form + 1. (-1) : Rule based on other pricelist + 2. (-2) : Rule based on supplierinfo + 3. (default) : Any price type which is set on the product form 3. An new option is introduced in the POS config to let the user show price with taxes in product widget. the UI is updated when we change the customer in order to adapt the prices. @@ -71,12 +71,12 @@ Implemented features at backend 1. Tax details -- Tax details per order line -- Tax details aggregated by tax at order level + - Tax details per order line + - Tax details aggregated by tax at order level 2. Ticket -- Tax details table added at end of printed ticket + - Tax details table added at end of printed ticket Known issues / Roadmap @@ -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/__openerp__.py b/pos_pricelist/__openerp__.py index 90954c31..bc83c7a5 100644 --- a/pos_pricelist/__openerp__.py +++ b/pos_pricelist/__openerp__.py @@ -31,4 +31,5 @@ ], 'post_init_hook': "set_pos_line_taxes", 'installable': True, + 'license': 'AGPL-3', } 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..a7b990b0 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,8 @@ 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..5d1278bd --- /dev/null +++ b/pos_pricelist/models/pos_order_patch.py @@ -0,0 +1,234 @@ +# coding: utf-8 +# Copyright: Odoo S.A. +# License: AGPL-3 +# flake8: noqa +# pylint: skip-file +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