test_anglo_saxon_valuation_reconciliation.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from freezegun import freeze_time
  4. from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
  5. from odoo.tests.common import Form, tagged
  6. from odoo import Command, fields
  7. @tagged('post_install', '-at_install')
  8. class TestValuationReconciliation(ValuationReconciliationTestCommon):
  9. @classmethod
  10. def setup_company_data(cls, company_name, chart_template=None, **kwargs):
  11. company_data = super().setup_company_data(company_name, chart_template=chart_template, **kwargs)
  12. # Create stock config.
  13. company_data.update({
  14. 'default_account_stock_price_diff': cls.env['account.account'].create({
  15. 'name': 'default_account_stock_price_diff',
  16. 'code': 'STOCKDIFF',
  17. 'reconcile': True,
  18. 'account_type': 'asset_current',
  19. 'company_id': company_data['company'].id,
  20. }),
  21. })
  22. return company_data
  23. def _create_purchase(self, product, date, quantity=1.0, set_tax=False, price_unit=66.0):
  24. with freeze_time(date):
  25. rslt = self.env['purchase.order'].create({
  26. 'partner_id': self.partner_a.id,
  27. 'currency_id': self.currency_data['currency'].id,
  28. 'order_line': [
  29. (0, 0, {
  30. 'name': product.name,
  31. 'product_id': product.id,
  32. 'product_qty': quantity,
  33. 'product_uom': product.uom_po_id.id,
  34. 'price_unit': price_unit,
  35. 'date_planned': date,
  36. 'taxes_id': [(6, 0, product.supplier_taxes_id.ids)] if set_tax else False,
  37. })],
  38. 'date_order': date,
  39. })
  40. rslt.button_confirm()
  41. return rslt
  42. def _create_invoice_for_po(self, purchase_order, date):
  43. with freeze_time(date):
  44. move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice', default_date=date))
  45. move_form.invoice_date = date
  46. move_form.partner_id = self.partner_a
  47. move_form.currency_id = self.currency_data['currency']
  48. move_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-purchase_order.id)
  49. return move_form.save()
  50. def test_shipment_invoice(self):
  51. """ Tests the case into which we receive the goods first, and then make the invoice.
  52. """
  53. test_product = self.test_product_delivery
  54. date_po_and_delivery = '2018-01-01'
  55. purchase_order = self._create_purchase(test_product, date_po_and_delivery)
  56. self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
  57. invoice = self._create_invoice_for_po(purchase_order, '2018-02-02')
  58. invoice.action_post()
  59. picking = self.env['stock.picking'].search([('purchase_id','=',purchase_order.id)])
  60. self.check_reconciliation(invoice, picking)
  61. # cancel the invoice
  62. invoice.button_cancel()
  63. def test_invoice_shipment(self):
  64. """ Tests the case into which we make the invoice first, and then receive the goods.
  65. """
  66. # Create a PO and an invoice for it
  67. test_product = self.test_product_order
  68. purchase_order = self._create_purchase(test_product, '2017-12-01')
  69. invoice = self._create_invoice_for_po(purchase_order, '2017-12-23')
  70. move_form = Form(invoice)
  71. with move_form.invoice_line_ids.edit(0) as line_form:
  72. line_form.quantity = 1
  73. invoice = move_form.save()
  74. # Validate the invoice and refund the goods
  75. invoice.action_post()
  76. self._process_pickings(purchase_order.picking_ids, date='2017-12-24')
  77. picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)])
  78. self.check_reconciliation(invoice, picking)
  79. # Return the goods and refund the invoice
  80. with freeze_time('2018-01-13'):
  81. stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(
  82. active_ids=picking.ids, active_id=picking.ids[0], active_model='stock.picking'))
  83. stock_return_picking = stock_return_picking_form.save()
  84. stock_return_picking.product_return_moves.quantity = 1.0
  85. stock_return_picking_action = stock_return_picking.create_returns()
  86. return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
  87. return_pick.action_assign()
  88. return_pick.move_ids.quantity_done = 1
  89. return_pick._action_done()
  90. # Refund the invoice
  91. refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=[invoice.id]).create({
  92. 'reason': 'test_invoice_shipment_refund',
  93. 'refund_method': 'cancel',
  94. 'date': '2018-03-15',
  95. 'journal_id': invoice.journal_id.id,
  96. })
  97. refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
  98. # Check the result
  99. self.assertEqual(invoice.payment_state, 'reversed', "Invoice should be in 'reversed' state")
  100. self.assertEqual(refund_invoice.payment_state, 'paid', "Refund should be in 'paid' state")
  101. self.check_reconciliation(refund_invoice, return_pick)
  102. def test_multiple_shipments_invoices(self):
  103. """ Tests the case into which we receive part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
  104. """
  105. test_product = self.test_product_delivery
  106. date_po_and_delivery0 = '2017-01-01'
  107. purchase_order = self._create_purchase(test_product, date_po_and_delivery0, quantity=5.0)
  108. self._process_pickings(purchase_order.picking_ids, quantity=2.0, date=date_po_and_delivery0)
  109. picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order="id asc", limit=1)
  110. invoice = self._create_invoice_for_po(purchase_order, '2017-01-15')
  111. move_form = Form(invoice)
  112. with move_form.invoice_line_ids.edit(0) as line_form:
  113. line_form.quantity = 3.0
  114. invoice = move_form.save()
  115. invoice.action_post()
  116. self.check_reconciliation(invoice, picking, full_reconcile=False)
  117. invoice2 = self._create_invoice_for_po(purchase_order, '2017-02-15')
  118. move_form = Form(invoice2)
  119. with move_form.invoice_line_ids.edit(0) as line_form:
  120. line_form.quantity = 2.0
  121. invoice2 = move_form.save()
  122. invoice2.action_post()
  123. self.check_reconciliation(invoice2, picking, full_reconcile=False)
  124. # We don't need to make the date of processing explicit since the very last rate
  125. # will be taken
  126. self._process_pickings(purchase_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
  127. picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)], order='id desc', limit=1)
  128. self.check_reconciliation(invoice2, picking)
  129. def test_rounding_discount(self):
  130. self.env.ref("product.decimal_discount").digits = 5
  131. tax_exclude_id = self.env["account.tax"].create(
  132. {
  133. "name": "Exclude tax",
  134. "amount": "0.00",
  135. "type_tax_use": "purchase",
  136. }
  137. )
  138. test_product = self.test_product_delivery
  139. test_product.supplier_taxes_id = [(6, 0, tax_exclude_id.ids)]
  140. date_po_and_delivery = '2018-01-01'
  141. purchase_order = self._create_purchase(test_product, date_po_and_delivery, quantity=10000, set_tax=True)
  142. self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
  143. invoice = self._create_invoice_for_po(purchase_order, '2018-01-01')
  144. # Set a discount
  145. move_form = Form(invoice)
  146. with move_form.invoice_line_ids.edit(0) as line_form:
  147. line_form.discount = 0.92431
  148. move_form.save()
  149. invoice.action_post()
  150. # Check the price difference amount.
  151. invoice_layer = self.env['stock.valuation.layer'].search([('account_move_line_id', 'in', invoice.line_ids.ids)])
  152. self.assertTrue(len(invoice_layer) == 1, "A price difference line should be created")
  153. self.assertAlmostEqual(invoice_layer.value, -3050.22)
  154. picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)])
  155. self.assertAlmostEqual(invoice_layer.value + picking.move_ids.stock_valuation_layer_ids.value, invoice.line_ids[0].debit)
  156. self.assertAlmostEqual(invoice_layer.value + picking.move_ids.stock_valuation_layer_ids.value, invoice.invoice_line_ids.price_subtotal/2, 2)
  157. self.check_reconciliation(invoice, picking)
  158. def test_rounding_price_unit(self):
  159. self.env.ref("product.decimal_price").digits = 6
  160. test_product = self.test_product_delivery
  161. date_po_and_delivery = '2018-01-01'
  162. purchase_order = self._create_purchase(test_product, date_po_and_delivery, quantity=1000000, price_unit=0.0005)
  163. self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
  164. invoice = self._create_invoice_for_po(purchase_order, '2018-01-01')
  165. # Set a discount
  166. move_form = Form(invoice)
  167. with move_form.invoice_line_ids.edit(0) as line_form:
  168. line_form.price_unit = 0.0006
  169. move_form.save()
  170. invoice.action_post()
  171. # Check the price difference amount. It's expected that price_unit * qty != price_total.
  172. invoice_layer = self.env['stock.valuation.layer'].search([('account_move_line_id', 'in', invoice.line_ids.ids)])
  173. self.assertTrue(len(invoice_layer) == 1, "A price difference line should be created")
  174. # self.assertAlmostEqual(invoice_layer.price_unit, 0.0001)
  175. self.assertAlmostEqual(invoice_layer.value, 50.0)
  176. picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)])
  177. self.check_reconciliation(invoice, picking)
  178. @freeze_time('2021-01-03')
  179. def test_price_difference_exchange_difference_accounting_date(self):
  180. self.stock_account_product_categ.property_account_creditor_price_difference_categ = self.company_data['default_account_stock_price_diff']
  181. test_product = self.test_product_delivery
  182. test_product.categ_id.write({"property_cost_method": "standard"})
  183. test_product.write({'standard_price': 100.0})
  184. date_po_receipt = '2021-01-02'
  185. rate_po_receipt = 25.0
  186. date_bill = '2021-01-01'
  187. rate_bill = 30.0
  188. date_accounting = '2021-01-03'
  189. rate_accounting = 26.0
  190. foreign_currency = self.currency_data['currency']
  191. company_currency = self.env.company.currency_id
  192. self.env['res.currency.rate'].create([
  193. {
  194. 'name': date_po_receipt,
  195. 'rate': rate_po_receipt,
  196. 'currency_id': foreign_currency.id,
  197. 'company_id': self.env.company.id,
  198. }, {
  199. 'name': date_bill,
  200. 'rate': rate_bill,
  201. 'currency_id': foreign_currency.id,
  202. 'company_id': self.env.company.id,
  203. }, {
  204. 'name': date_accounting,
  205. 'rate': rate_accounting,
  206. 'currency_id': foreign_currency.id,
  207. 'company_id': self.env.company.id,
  208. }, {
  209. 'name': date_po_receipt,
  210. 'rate': 1.0,
  211. 'currency_id': company_currency.id,
  212. 'company_id': self.env.company.id,
  213. }, {
  214. 'name': date_accounting,
  215. 'rate': 1.0,
  216. 'currency_id': company_currency.id,
  217. 'company_id': self.env.company.id,
  218. }, {
  219. 'name': date_bill,
  220. 'rate': 1.0,
  221. 'currency_id': company_currency.id,
  222. 'company_id': self.env.company.id,
  223. }])
  224. #purchase order created in foreign currency
  225. purchase_order = self._create_purchase(test_product, date_po_receipt, quantity=10, price_unit=3000)
  226. with freeze_time(date_po_receipt):
  227. self._process_pickings(purchase_order.picking_ids)
  228. invoice = self._create_invoice_for_po(purchase_order, date_bill)
  229. with Form(invoice) as move_form:
  230. move_form.invoice_date = fields.Date.from_string(date_bill)
  231. move_form.date = fields.Date.from_string(date_accounting)
  232. invoice.action_post()
  233. price_diff_line = invoice.line_ids.filtered(lambda l: l.account_id == self.stock_account_product_categ.property_account_creditor_price_difference_categ)
  234. self.assertTrue(len(price_diff_line) == 1, "A price difference line should be created")
  235. self.assertAlmostEqual(price_diff_line.balance, 192.31)
  236. self.assertAlmostEqual(price_diff_line.price_subtotal, 5000.0)
  237. picking = self.env['stock.picking'].search([('purchase_id', '=', purchase_order.id)])
  238. interim_account_id = self.company_data['default_account_stock_in'].id
  239. valuation_line = picking.move_ids.mapped('account_move_ids.line_ids').filtered(lambda x: x.account_id.id == interim_account_id)
  240. self.assertTrue(valuation_line.full_reconcile_id, "The reconciliation should be total at that point.")
  241. def test_reconcile_cash_basis_bill(self):
  242. ''' Test the generation of the CABA move after bill payment
  243. '''
  244. self.env.company.tax_exigibility = True
  245. cash_basis_base_account = self.env['account.account'].create({
  246. 'code': 'cash.basis.base.account',
  247. 'name': 'cash_basis_base_account',
  248. 'account_type': 'income',
  249. 'company_id': self.company_data['company'].id,
  250. })
  251. self.company_data['company'].account_cash_basis_base_account_id = cash_basis_base_account
  252. cash_basis_transfer_account = self.env['account.account'].create({
  253. 'code': 'cash.basis.transfer.account',
  254. 'name': 'cash_basis_transfer_account',
  255. 'account_type': 'income',
  256. 'company_id': self.company_data['company'].id,
  257. })
  258. tax_account_1 = self.env['account.account'].create({
  259. 'code': 'tax.account.1',
  260. 'name': 'tax_account_1',
  261. 'account_type': 'income',
  262. 'company_id': self.company_data['company'].id,
  263. })
  264. tax_tags = self.env['account.account.tag'].create({
  265. 'name': 'tax_tag_%s' % str(i),
  266. 'applicability': 'taxes',
  267. } for i in range(8))
  268. cash_basis_tax_a_third_amount = self.env['account.tax'].create({
  269. 'name': 'tax_1',
  270. 'amount': 33.3333,
  271. 'company_id': self.company_data['company'].id,
  272. 'cash_basis_transition_account_id': cash_basis_transfer_account.id,
  273. 'tax_exigibility': 'on_payment',
  274. 'invoice_repartition_line_ids': [
  275. (0, 0, {
  276. 'repartition_type': 'base',
  277. 'tag_ids': [(6, 0, tax_tags[0].ids)],
  278. }),
  279. (0, 0, {
  280. 'repartition_type': 'tax',
  281. 'account_id': tax_account_1.id,
  282. 'tag_ids': [(6, 0, tax_tags[1].ids)],
  283. }),
  284. ],
  285. 'refund_repartition_line_ids': [
  286. (0, 0, {
  287. 'repartition_type': 'base',
  288. 'tag_ids': [(6, 0, tax_tags[2].ids)],
  289. }),
  290. (0, 0, {
  291. 'repartition_type': 'tax',
  292. 'account_id': tax_account_1.id,
  293. 'tag_ids': [(6, 0, tax_tags[3].ids)],
  294. }),
  295. ],
  296. })
  297. product_A = self.env["product.product"].create(
  298. {
  299. "name": "Product A",
  300. "type": "product",
  301. "default_code": "prda",
  302. "categ_id": self.stock_account_product_categ.id,
  303. "taxes_id": [(5, 0, 0)],
  304. "supplier_taxes_id": [(6, 0, cash_basis_tax_a_third_amount.ids)],
  305. "lst_price": 100.0,
  306. "standard_price": 10.0,
  307. "property_account_income_id": self.company_data["default_account_revenue"].id,
  308. "property_account_expense_id": self.company_data["default_account_expense"].id,
  309. }
  310. )
  311. product_A.categ_id.write(
  312. {
  313. "property_valuation": "real_time",
  314. "property_cost_method": "standard",
  315. }
  316. )
  317. date_po_and_delivery = '2018-01-01'
  318. purchase_order = self._create_purchase(product_A, date_po_and_delivery, set_tax=True, price_unit=300.0)
  319. self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
  320. bill = self._create_invoice_for_po(purchase_order, '2018-02-02')
  321. bill.action_post()
  322. # Register a payment creating the CABA journal entry on the fly and reconcile it with the tax line.
  323. self.env['account.payment.register']\
  324. .with_context(active_ids=bill.ids, active_model='account.move')\
  325. .create({})\
  326. ._create_payments()
  327. partial_rec = bill.mapped('line_ids.matched_debit_ids')
  328. caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
  329. # Tax values based on payment
  330. # Invoice amount 300
  331. self.assertRecordValues(caba_move.line_ids, [
  332. # pylint: disable=C0326
  333. # Base amount:
  334. {'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'account_id': cash_basis_base_account.id},
  335. {'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'account_id': cash_basis_base_account.id},
  336. # tax:
  337. {'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'account_id': cash_basis_transfer_account.id},
  338. {'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'account_id': tax_account_1.id},
  339. ])
  340. def test_reconciliation_differed_billing(self):
  341. """
  342. Test whether products received billed at different time will be correctly reconciled
  343. valuation: automated
  344. - create a rfq
  345. - receive products
  346. - create bill - set quantity of product A = 0 - save
  347. - create bill - confirm
  348. -> the reconciliation should not take into account the lines of the first bill
  349. """
  350. date_po_and_delivery = '2022-03-02'
  351. self.product_a.write({
  352. 'categ_id': self.stock_account_product_categ,
  353. 'detailed_type': 'product',
  354. })
  355. self.product_b.write({
  356. 'categ_id': self.stock_account_product_categ,
  357. 'detailed_type': 'product',
  358. })
  359. purchase_order = self.env['purchase.order'].create({
  360. 'currency_id': self.currency_data['currency'].id,
  361. 'order_line': [
  362. Command.create({
  363. 'name': self.product_a.name,
  364. 'product_id': self.product_a.id,
  365. 'product_qty': 1,
  366. }),
  367. Command.create({
  368. 'name': self.product_b.name,
  369. 'product_id': self.product_b.id,
  370. 'product_qty': 1,
  371. }),
  372. ],
  373. 'partner_id': self.partner_a.id,
  374. })
  375. purchase_order.button_confirm()
  376. self._process_pickings(purchase_order.picking_ids, date=date_po_and_delivery)
  377. bill_1 = self._create_invoice_for_po(purchase_order, date_po_and_delivery)
  378. move_form = Form(bill_1)
  379. with move_form.invoice_line_ids.edit(0) as line_form:
  380. line_form.quantity = 0
  381. move_form.save()
  382. bill_2 = self._create_invoice_for_po(purchase_order, date=date_po_and_delivery)
  383. bill_2.action_post()
  384. aml = bill_2.line_ids.filtered(lambda line: line.display_type == "product")
  385. pol = purchase_order.order_line
  386. self.assertRecordValues(pol, [{'qty_invoiced': line.qty_received} for line in pol])
  387. self.assertRecordValues(aml, [{'reconciled': True} for line in aml])