From 2d73ea9bed1c6f5c801d73776e223e2be84d25f6 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Tue, 6 Feb 2018 11:18:55 +0100 Subject: [PATCH] [FIX] point_of_sale, stock_account: POS Anglo-Saxon Accounting Anglo-Saxon Accounting entries are not created when the POS session was closed with button "Close and post entries opw:802727 --- addons/account/models/account_invoice.py | 43 +++--- addons/point_of_sale/__init__.py | 1 + addons/point_of_sale/point_of_sale.py | 36 +++++ addons/point_of_sale/tests/__init__.py | 4 + .../point_of_sale/tests/test_anglo_saxon.py | 99 ++++++++++++++ addons/stock_account/stock_account.py | 125 +++++++++++------- 6 files changed, 240 insertions(+), 68 deletions(-) create mode 100644 addons/point_of_sale/tests/__init__.py create mode 100644 addons/point_of_sale/tests/test_anglo_saxon.py diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index f653941e7b6..11b2cf6551a 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -859,24 +859,7 @@ class AccountInvoice(models.Model): @api.model def line_get_convert(self, line, part): - return { - 'date_maturity': line.get('date_maturity', False), - 'partner_id': part, - 'name': line['name'], - 'debit': line['price'] > 0 and line['price'], - 'credit': line['price'] < 0 and -line['price'], - 'account_id': line['account_id'], - 'analytic_line_ids': line.get('analytic_line_ids', []), - 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), - 'currency_id': line.get('currency_id', False), - 'quantity': line.get('quantity', 1.00), - 'product_id': line.get('product_id', False), - 'product_uom_id': line.get('uom_id', False), - 'analytic_account_id': line.get('account_analytic_id', False), - 'invoice_id': line.get('invoice_id', False), - 'tax_ids': line.get('tax_ids', False), - 'tax_line_id': line.get('tax_line_id', False), - } + return self.env['product.product']._convert_prepared_anglosaxon_line(line, part) @api.multi def action_cancel(self): @@ -1455,3 +1438,27 @@ class MailComposeMessage(models.Model): invoice.sent = True invoice.message_post(body=_("Invoice sent")) return super(MailComposeMessage, self).send_mail(auto_commit=auto_commit) + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.model + def _convert_prepared_anglosaxon_line(self, line, partner): + return { + 'date_maturity': line.get('date_maturity', False), + 'partner_id': partner, + 'name': line['name'], + 'debit': line['price'] > 0 and line['price'], + 'credit': line['price'] < 0 and -line['price'], + 'account_id': line['account_id'], + 'analytic_line_ids': line.get('analytic_line_ids', []), + 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), + 'currency_id': line.get('currency_id', False), + 'quantity': line.get('quantity', 1.00), + 'product_id': line.get('product_id', False), + 'product_uom_id': line.get('uom_id', False), + 'analytic_account_id': line.get('account_analytic_id', False), + 'invoice_id': line.get('invoice_id', False), + 'tax_ids': line.get('tax_ids', False), + 'tax_line_id': line.get('tax_line_id', False), + } diff --git a/addons/point_of_sale/__init__.py b/addons/point_of_sale/__init__.py index f1451fa6494..c3625eb3e3e 100644 --- a/addons/point_of_sale/__init__.py +++ b/addons/point_of_sale/__init__.py @@ -9,3 +9,4 @@ import res_users import res_partner import wizard import res_config +import tests diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 7e94c36ff31..09a986c1b64 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -1319,7 +1319,40 @@ class pos_order(osv.osv): grouped_data = {} have_to_group_by = session and session.config_id.group_by or False rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method + def add_anglosaxon_lines(grouped_data): + Product = self.pool['product.product'] + Analytic = self.pool['account.analytic.account'] + for product_key in list(grouped_data.keys()): + if product_key[0] == "product": + line = grouped_data[product_key][0] + product = Product.browse(cr, uid, line['product_id'], context=context) + # In the SO part, the entries will be inverted by function compute_invoice_totals + price_unit = - product._get_anglo_saxon_price_unit() + account_analytic = Analytic.browse(cr, uid, line.get('analytic_account_id'), context=context) + res = Product._anglo_saxon_sale_move_lines(cr, uid, + line['name'], product, product.uom_id, line['quantity'], price_unit, + fiscal_position=order.fiscal_position_id, + account_analytic=account_analytic, context=context) + if res: + line1, line2 = res + line1 = Product._convert_prepared_anglosaxon_line(cr, uid, line1, order.partner_id, context=context) + insert_data('counter_part', { + 'name': line1['name'], + 'account_id': line1['account_id'], + 'credit': line1['credit'] or 0.0, + 'debit': line1['debit'] or 0.0, + 'partner_id': line1['partner_id'] + }) + + line2 = Product._convert_prepared_anglosaxon_line(cr, uid, line2, order.partner_id, context=context) + insert_data('counter_part', { + 'name': line2['name'], + 'account_id': line2['account_id'], + 'credit': line2['credit'] or 0.0, + 'debit': line2['debit'] or 0.0, + 'partner_id': line2['partner_id'] + }) for order in self.browse(cr, uid, ids, context=context): if order.account_move: continue @@ -1463,6 +1496,9 @@ class pos_order(osv.osv): order.write({'state':'done', 'account_move': move_id}) + if ids and order.company_id.anglo_saxon_accounting: + add_anglosaxon_lines(grouped_data) + all_lines = [] for group_key, group_data in grouped_data.iteritems(): for value in group_data: diff --git a/addons/point_of_sale/tests/__init__.py b/addons/point_of_sale/tests/__init__.py new file mode 100644 index 00000000000..4136a24505f --- /dev/null +++ b/addons/point_of_sale/tests/__init__.py @@ -0,0 +1,4 @@ +#Accounting tests written in python should extend the class AccountingTestCase. +#See its doc for more info. + +from . import test_anglo_saxon \ No newline at end of file diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py new file mode 100644 index 00000000000..a8d2ec34339 --- /dev/null +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +import openerp +import time +from openerp import fields + +from openerp.tests import common + + +class TestAngloSaxonCommon(common.TransactionCase): + + def setUp(self): + super(TestAngloSaxonCommon, self).setUp() + self.PosMakePayment = self.env['pos.make.payment'] + self.PosOrder = self.env['pos.order'] + self.Statement = self.env['account.bank.statement'] + self.company = self.env.ref('base.main_company') + self.product = self.env.ref('product.product_product_3') + self.partner = self.env.ref('base.res_partner_1') + self.category = self.env.ref('product.product_category_1') + self.category = self.category.copy({'name': 'New category','property_valuation': 'real_time'}) + account_type = self.env['account.account.type'].create({'name': 'RCV type', 'type': 'receivable'}) + self.account = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00' , 'user_type_id': account_type.id, 'reconcile': True}) + account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00' , 'user_type_id': account_type.id, 'reconcile': True}) + account_output = self.env['account.account'].create({'name': 'Output', 'code': 'OUT00' , 'user_type_id': account_type.id, 'reconcile': True}) + self.partner.property_account_receivable_id = self.account + self.category.property_account_income_categ_id = self.account + self.category.property_account_expense_categ_id = account_expense + self.category.property_stock_account_input_categ_id = self.account + self.category.property_stock_account_output_categ_id = account_output + self.category.property_stock_valuation_account_id = self.account + self.category.property_stock_journal = self.env['account.journal'].create({'name': 'Stock journal', 'type': 'sale', 'code': 'STK00'}) + self.pos_config = self.env.ref('point_of_sale.pos_config_main') + self.pos_config = self.pos_config.copy({'name': 'New POS config'}) + self.product = self.product.copy({'name': 'New product','standard_price': 100}) + self.company.anglo_saxon_accounting = True + self.product.categ_id = self.category + sale_journal = self.env['account.journal'].create({'name': 'POS journal', 'type': 'sale', 'code': 'POS00'}) + self.pos_config.journal_id = sale_journal + self.cash_journal = self.env['account.journal'].create({'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'}) + + +class TestAngloSaxonFlow(TestAngloSaxonCommon): + + def test_create_account_move_line(self): + # This test will check that the correct journal entries are created when a product in real time valuation + # is sold in a company using anglo-saxon + self.pos_config.open_session_cb() + self.pos_config.current_session_id.write({'journal_ids': [(6, 0, [self.cash_journal.id])]}) + self.cash_journal.loss_account_id = self.account + self.pos_statement = self.Statement.create({ + 'balance_start': 0.0, + 'balance_end_real': 0.0, + 'date': time.strftime('%Y-%m-%d'), + 'journal_id': self.cash_journal.id, + 'company_id': self.company.id, + 'name': 'pos session test', + }) + self.pos_config.current_session_id.write({'statement_ids': [(6, 0, [self.pos_statement.id])]}) + + # I create a PoS order with 1 unit of New product at 450 EUR + self.pos_order_pos0 = self.PosOrder.create({ + 'company_id': self.company.id, + 'partner_id': self.partner.id, + 'pricelist_id': self.company.partner_id.property_product_pricelist.id, + 'session_id': self.pos_config.current_session_id.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 1.0 + })] + }) + + # I make a payment to fully pay the order + context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 450.0 + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.') + + # I close the current session to generate the journal entries + self.pos_config.current_session_id.wkf_action_close() + + # I test that the generated journal entries are correct. + account_output = self.category.property_stock_account_output_categ_id + expense_account = self.category.property_account_expense_categ_id + aml = self.pos_order_pos0.account_move.line_ids + aml_output = aml.filtered(lambda l: l.account_id.id == account_output.id) + aml_expense = aml.filtered(lambda l: l.account_id.id == expense_account.id) + self.assertEqual(aml_output.credit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching") + self.assertEqual(aml_expense.debit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching") diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 24c87be86f5..88c1c1edaf2 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -24,17 +24,78 @@ class stock_inventory(osv.osv): ctx['force_period_date'] = inv.accounting_date return super(stock_inventory, self).post_inventory(cr, uid, inv, context=ctx) +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.model + def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False): + """Prepare dicts describing new journal COGS journal items for a product sale. + + Returns a dict that should be passed to `_convert_prepared_anglosaxon_line()` to + obtain the creation value for the new journal items. + + :param Model product: a product.product record of the product being sold + :param Model uom: a product.uom record of the UoM of the sale line + :param Integer qty: quantity of the product being sold + :param Integer price_unit: unit price of the product being sold + :param Model currency: a res.currency record from the order of the product being sold + :param Interger amount_currency: unit price in the currency from the order of the product being sold + :param Model fiscal_position: a account.fiscal.position record from the order of the product being sold + :param Model account_analytic: a account.account.analytic record from the line of the product being sold + """ + + if product.type == 'product' and product.valuation == 'real_time': + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fiscal_position) + # debit account dacc will be the output account + dacc = accounts['stock_output'].id + # credit account cacc will be the expense account + cacc = accounts['expense'].id + if dacc and cacc: + return [ + { + 'type': 'src', + 'name': name[:64], + 'price_unit': price_unit, + 'quantity': qty, + 'price': price_unit * qty, + 'currency_id': currency and currency.id, + 'amount_currency': amount_currency, + 'account_id': dacc, + 'product_id': product.id, + 'uom_id': uom.id, + 'account_analytic_id': account_analytic and account_analytic.id, + }, + + { + 'type': 'src', + 'name': name[:64], + 'price_unit': price_unit, + 'quantity': qty, + 'price': -1 * price_unit * qty, + 'currency_id': currency and currency.id, + 'amount_currency': -1 * amount_currency, + 'account_id': cacc, + 'product_id': product.id, + 'uom_id': uom.id, + 'account_analytic_id': account_analytic and account_analytic.id, + }, + ] + return [] + + @api.model + def _get_anglo_saxon_price_unit(self, uom=False): + price = self.standard_price + if not uom or self.uom_id.id == uom.id: + return price + return self.uom_id._compute_price(self.uom_id.id, price, to_uom_id=uom.id) + class account_invoice_line(osv.osv): _inherit = "account.invoice.line" def _get_anglo_saxon_price_unit(self): self.ensure_one() - price = self.product_id.standard_price - if not self.uom_id or self.product_id.uom_id == self.uom_id: - return price - else: - return self.product_id.uom_id._compute_price(self.product_id.uom_id.id, price, to_uom_id=self.uom_id.id) + return self.product_id._get_anglo_saxon_price_unit(uom=self.uom_id) def _get_price(self, cr, uid, inv, company_currency, i_line, price_unit): cur_obj = self.pool.get('res.currency') @@ -72,53 +133,17 @@ class account_invoice(osv.osv): res: The move line entries produced so far by the parent move_line_get. """ inv = i_line.invoice_id - company_currency = inv.company_id.currency_id.id + company_currency = inv.company_id.currency_id + price_unit = i_line._get_anglo_saxon_price_unit() + if inv.currency_id != company_currency: + currency_id = inv.currency_id.id + amount_currency = i_line._get_price(inv, company_currency.id, i_line, price_unit) + else: + currency_id = False + amount_currency = False - if i_line.product_id.type in ('product', 'consu') and i_line.product_id.valuation == 'real_time': - fpos = i_line.invoice_id.fiscal_position_id - accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) - # debit account dacc will be the output account - dacc = accounts['stock_output'].id - # credit account cacc will be the expense account - cacc = accounts['expense'].id - if dacc and cacc: - price_unit = i_line._get_anglo_saxon_price_unit() - if inv.currency_id.id != company_currency: - currency_id = inv.currency_id.id - amount_currency = self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit) - else: - currency_id = False - amount_currency = False - return [ - { - 'type':'src', - 'name': i_line.name[:64], - 'price_unit': price_unit, - 'quantity': i_line.quantity, - 'price': price_unit * i_line.quantity, - 'currency_id': currency_id, - 'amount_currency': amount_currency, - 'account_id':dacc, - 'product_id':i_line.product_id.id, - 'uom_id':i_line.uom_id.id, - 'account_analytic_id': i_line.account_analytic_id.id, - }, + return self.env['product.product']._anglo_saxon_sale_move_lines(i_line.name, i_line.product_id, i_line.uom_id, i_line.quantity, price_unit, currency=currency_id, amount_currency=amount_currency, fiscal_position=inv.fiscal_position_id, account_analytic=i_line.account_analytic_id) - { - 'type':'src', - 'name': i_line.name[:64], - 'price_unit': price_unit, - 'quantity': i_line.quantity, - 'price': -1 * price_unit * i_line.quantity, - 'currency_id': currency_id, - 'amount_currency': -1 * amount_currency, - 'account_id':cacc, - 'product_id':i_line.product_id.id, - 'uom_id':i_line.uom_id.id, - 'account_analytic_id': i_line.account_analytic_id.id, - }, - ] - return [] #---------------------------------------------------------- # Stock Location