test_point_of_sale_flow.py 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import time
  4. from freezegun import freeze_time
  5. from datetime import datetime
  6. import odoo
  7. from odoo import fields, tools
  8. from odoo.tools import float_compare, mute_logger, test_reports
  9. from odoo.tests.common import Form
  10. from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon
  11. @odoo.tests.tagged('post_install', '-at_install')
  12. class TestPointOfSaleFlow(TestPointOfSaleCommon):
  13. def compute_tax(self, product, price, qty=1, taxes=None):
  14. if not taxes:
  15. taxes = product.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id)
  16. currency = self.pos_config.pricelist_id.currency_id
  17. res = taxes.compute_all(price, currency, qty, product=product)
  18. untax = res['total_excluded']
  19. return untax, sum(tax.get('amount', 0.0) for tax in res['taxes'])
  20. def test_order_refund(self):
  21. self.pos_config.open_ui()
  22. current_session = self.pos_config.current_session_id
  23. # I create a new PoS order with 2 lines
  24. order = self.PosOrder.create({
  25. 'company_id': self.env.company.id,
  26. 'session_id': current_session.id,
  27. 'partner_id': self.partner1.id,
  28. 'pricelist_id': self.partner1.property_product_pricelist.id,
  29. 'lines': [(0, 0, {
  30. 'name': "OL/0001",
  31. 'product_id': self.product3.id,
  32. 'price_unit': 450,
  33. 'discount': 5.0,
  34. 'qty': 2.0,
  35. 'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
  36. 'price_subtotal': 450 * (1 - 5/100.0) * 2,
  37. 'price_subtotal_incl': 450 * (1 - 5/100.0) * 2,
  38. }), (0, 0, {
  39. 'name': "OL/0002",
  40. 'product_id': self.product4.id,
  41. 'price_unit': 300,
  42. 'discount': 5.0,
  43. 'qty': 3.0,
  44. 'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
  45. 'price_subtotal': 300 * (1 - 5/100.0) * 3,
  46. 'price_subtotal_incl': 300 * (1 - 5/100.0) * 3,
  47. })],
  48. 'amount_total': 1710.0,
  49. 'amount_tax': 0.0,
  50. 'amount_paid': 0.0,
  51. 'amount_return': 0.0,
  52. })
  53. payment_context = {"active_ids": order.ids, "active_id": order.id}
  54. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  55. 'amount': order.amount_total,
  56. 'payment_method_id': self.cash_payment_method.id
  57. })
  58. order_payment.with_context(**payment_context).check()
  59. self.assertAlmostEqual(order.amount_total, order.amount_paid, msg='Order should be fully paid.')
  60. # I create a refund
  61. refund_action = order.refund()
  62. refund = self.PosOrder.browse(refund_action['res_id'])
  63. self.assertEqual(order.amount_total, -1*refund.amount_total,
  64. "The refund does not cancel the order (%s and %s)" % (order.amount_total, refund.amount_total))
  65. payment_context = {"active_ids": refund.ids, "active_id": refund.id}
  66. refund_payment = self.PosMakePayment.with_context(**payment_context).create({
  67. 'amount': refund.amount_total,
  68. 'payment_method_id': self.cash_payment_method.id,
  69. })
  70. # I click on the validate button to register the payment.
  71. refund_payment.with_context(**payment_context).check()
  72. self.assertEqual(refund.state, 'paid', "The refund is not marked as paid")
  73. self.assertTrue(refund.payment_ids.payment_method_id.is_cash_count, msg='There should only be one payment and paid in cash.')
  74. total_cash_payment = sum(current_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount'))
  75. current_session.post_closing_cash_details(total_cash_payment)
  76. current_session.close_session_from_ui()
  77. self.assertEqual(current_session.state, 'closed', msg='State of current session should be closed.')
  78. def test_order_refund_lots(self):
  79. # open pos session
  80. self.pos_config.open_ui()
  81. current_session = self.pos_config.current_session_id
  82. # set up product iwith SN tracing and create two lots (1001, 1002)
  83. self.stock_location = self.company_data['default_warehouse'].lot_stock_id
  84. self.product2 = self.env['product.product'].create({
  85. 'name': 'Product A',
  86. 'type': 'product',
  87. 'tracking': 'serial',
  88. 'categ_id': self.env.ref('product.product_category_all').id,
  89. })
  90. lot1 = self.env['stock.lot'].create({
  91. 'name': '1001',
  92. 'product_id': self.product2.id,
  93. 'company_id': self.env.company.id,
  94. })
  95. lot2 = self.env['stock.lot'].create({
  96. 'name': '1002',
  97. 'product_id': self.product2.id,
  98. 'company_id': self.env.company.id,
  99. })
  100. self.env['stock.quant'].with_context(inventory_mode=True).create({
  101. 'product_id': self.product2.id,
  102. 'inventory_quantity': 1,
  103. 'location_id': self.stock_location.id,
  104. 'lot_id': lot1.id
  105. }).action_apply_inventory()
  106. self.env['stock.quant'].with_context(inventory_mode=True).create({
  107. 'product_id': self.product2.id,
  108. 'inventory_quantity': 1,
  109. 'location_id': self.stock_location.id,
  110. 'lot_id': lot2.id
  111. }).action_apply_inventory()
  112. # create pos order with the two SN created before
  113. order = self.PosOrder.create({
  114. 'company_id': self.env.company.id,
  115. 'session_id': current_session.id,
  116. 'partner_id': self.partner1.id,
  117. 'lines': [(0, 0, {
  118. 'name': "OL/0001",
  119. 'product_id': self.product2.id,
  120. 'price_unit': 6,
  121. 'discount': 0,
  122. 'qty': 2,
  123. 'tax_ids': [[6, False, []]],
  124. 'price_subtotal': 12,
  125. 'price_subtotal_incl': 12,
  126. 'pack_lot_ids': [
  127. [0, 0, {'lot_name': '1001'}],
  128. [0, 0, {'lot_name': '1002'}],
  129. ]
  130. })],
  131. 'pricelist_id': self.pos_config.pricelist_id.id,
  132. 'amount_paid': 12.0,
  133. 'amount_total': 12.0,
  134. 'amount_tax': 0.0,
  135. 'amount_return': 0.0,
  136. 'to_invoice': False,
  137. })
  138. payment_context = {"active_ids": order.ids, "active_id": order.id}
  139. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  140. 'amount': order.amount_total,
  141. 'payment_method_id': self.cash_payment_method.id
  142. })
  143. order_payment.with_context(**payment_context).check()
  144. # I create a refund
  145. refund_action = order.refund()
  146. refund = self.PosOrder.browse(refund_action['res_id'])
  147. order_lot_id = [lot_id.lot_name for lot_id in order.lines.pack_lot_ids]
  148. refund_lot_id = [lot_id.lot_name for lot_id in refund.lines.pack_lot_ids]
  149. self.assertEqual(
  150. order_lot_id,
  151. refund_lot_id,
  152. "In the refund we should find the same lot as in the original order")
  153. payment_context = {"active_ids": refund.ids, "active_id": refund.id}
  154. refund_payment = self.PosMakePayment.with_context(**payment_context).create({
  155. 'amount': refund.amount_total,
  156. 'payment_method_id': self.cash_payment_method.id,
  157. })
  158. # I click on the validate button to register the payment.
  159. refund_payment.with_context(**payment_context).check()
  160. self.assertEqual(refund.state, 'paid', "The refund is not marked as paid")
  161. current_session.action_pos_session_closing_control()
  162. def test_order_to_picking(self):
  163. """
  164. In order to test the Point of Sale in module, I will do three orders from the sale to the payment,
  165. invoicing + picking, but will only check the picking consistency in the end.
  166. TODO: Check the negative picking after changing the picking relation to One2many (also for a mixed use case),
  167. check the quantity, the locations and return picking logic
  168. """
  169. # I click on create a new session button
  170. self.pos_config.open_ui()
  171. current_session = self.pos_config.current_session_id
  172. # I create a PoS order with 2 units of PCSC234 at 450 EUR
  173. # and 3 units of PCSC349 at 300 EUR.
  174. untax1, atax1 = self.compute_tax(self.product3, 450, 2)
  175. untax2, atax2 = self.compute_tax(self.product4, 300, 3)
  176. self.pos_order_pos1 = self.PosOrder.create({
  177. 'company_id': self.env.company.id,
  178. 'session_id': current_session.id,
  179. 'pricelist_id': self.partner1.property_product_pricelist.id,
  180. 'partner_id': self.partner1.id,
  181. 'lines': [(0, 0, {
  182. 'name': "OL/0001",
  183. 'product_id': self.product3.id,
  184. 'price_unit': 450,
  185. 'discount': 0.0,
  186. 'qty': 2.0,
  187. 'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
  188. 'price_subtotal': untax1,
  189. 'price_subtotal_incl': untax1 + atax1,
  190. }), (0, 0, {
  191. 'name': "OL/0002",
  192. 'product_id': self.product4.id,
  193. 'price_unit': 300,
  194. 'discount': 0.0,
  195. 'qty': 3.0,
  196. 'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
  197. 'price_subtotal': untax2,
  198. 'price_subtotal_incl': untax2 + atax2,
  199. })],
  200. 'amount_tax': atax1 + atax2,
  201. 'amount_total': untax1 + untax2 + atax1 + atax2,
  202. 'amount_paid': 0,
  203. 'amount_return': 0,
  204. })
  205. context_make_payment = {
  206. "active_ids": [self.pos_order_pos1.id],
  207. "active_id": self.pos_order_pos1.id
  208. }
  209. self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({
  210. 'amount': untax1 + untax2 + atax1 + atax2
  211. })
  212. # I click on the validate button to register the payment.
  213. context_payment = {'active_id': self.pos_order_pos1.id}
  214. self.pos_make_payment_2.with_context(context_payment).check()
  215. # I check that the order is marked as paid
  216. self.assertEqual(
  217. self.pos_order_pos1.state,
  218. 'paid',
  219. 'Order should be in paid state.'
  220. )
  221. # I test that the pickings are created as expected during payment
  222. # One picking attached and having all the positive move lines in the correct state
  223. self.assertEqual(
  224. self.pos_order_pos1.picking_ids[0].state,
  225. 'done',
  226. 'Picking should be in done state.'
  227. )
  228. self.assertEqual(
  229. self.pos_order_pos1.picking_ids[0].move_ids.mapped('state'),
  230. ['done', 'done'],
  231. 'Move Lines should be in done state.'
  232. )
  233. # I create a second order
  234. untax1, atax1 = self.compute_tax(self.product3, 450, -2)
  235. untax2, atax2 = self.compute_tax(self.product4, 300, -3)
  236. self.pos_order_pos2 = self.PosOrder.create({
  237. 'company_id': self.env.company.id,
  238. 'session_id': current_session.id,
  239. 'pricelist_id': self.partner1.property_product_pricelist.id,
  240. 'partner_id': self.partner1.id,
  241. 'lines': [(0, 0, {
  242. 'name': "OL/0003",
  243. 'product_id': self.product3.id,
  244. 'price_unit': 450,
  245. 'discount': 0.0,
  246. 'qty': (-2.0),
  247. 'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
  248. 'price_subtotal': untax1,
  249. 'price_subtotal_incl': untax1 + atax1,
  250. }), (0, 0, {
  251. 'name': "OL/0004",
  252. 'product_id': self.product4.id,
  253. 'price_unit': 300,
  254. 'discount': 0.0,
  255. 'qty': (-3.0),
  256. 'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
  257. 'price_subtotal': untax2,
  258. 'price_subtotal_incl': untax2 + atax2,
  259. })],
  260. 'amount_tax': atax1 + atax2,
  261. 'amount_total': untax1 + untax2 + atax1 + atax2,
  262. 'amount_paid': 0,
  263. 'amount_return': 0,
  264. })
  265. context_make_payment = {
  266. "active_ids": [self.pos_order_pos2.id],
  267. "active_id": self.pos_order_pos2.id
  268. }
  269. self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({
  270. 'amount': untax1 + untax2 + atax1 + atax2
  271. })
  272. # I click on the validate button to register the payment.
  273. context_payment = {'active_id': self.pos_order_pos2.id}
  274. self.pos_make_payment_3.with_context(context_payment).check()
  275. # I check that the order is marked as paid
  276. self.assertEqual(
  277. self.pos_order_pos2.state,
  278. 'paid',
  279. 'Order should be in paid state.'
  280. )
  281. # I test that the pickings are created as expected
  282. # One picking attached and having all the positive move lines in the correct state
  283. self.assertEqual(
  284. self.pos_order_pos2.picking_ids[0].state,
  285. 'done',
  286. 'Picking should be in done state.'
  287. )
  288. self.assertEqual(
  289. self.pos_order_pos2.picking_ids[0].move_ids.mapped('state'),
  290. ['done', 'done'],
  291. 'Move Lines should be in done state.'
  292. )
  293. untax1, atax1 = self.compute_tax(self.product3, 450, -2)
  294. untax2, atax2 = self.compute_tax(self.product4, 300, 3)
  295. self.pos_order_pos3 = self.PosOrder.create({
  296. 'company_id': self.env.company.id,
  297. 'session_id': current_session.id,
  298. 'pricelist_id': self.partner1.property_product_pricelist.id,
  299. 'partner_id': self.partner1.id,
  300. 'lines': [(0, 0, {
  301. 'name': "OL/0005",
  302. 'product_id': self.product3.id,
  303. 'price_unit': 450,
  304. 'discount': 0.0,
  305. 'qty': (-2.0),
  306. 'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
  307. 'price_subtotal': untax1,
  308. 'price_subtotal_incl': untax1 + atax1,
  309. }), (0, 0, {
  310. 'name': "OL/0006",
  311. 'product_id': self.product4.id,
  312. 'price_unit': 300,
  313. 'discount': 0.0,
  314. 'qty': 3.0,
  315. 'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
  316. 'price_subtotal': untax2,
  317. 'price_subtotal_incl': untax2 + atax2,
  318. })],
  319. 'amount_tax': atax1 + atax2,
  320. 'amount_total': untax1 + untax2 + atax1 + atax2,
  321. 'amount_paid': 0,
  322. 'amount_return': 0,
  323. })
  324. context_make_payment = {
  325. "active_ids": [self.pos_order_pos3.id],
  326. "active_id": self.pos_order_pos3.id
  327. }
  328. self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({
  329. 'amount': untax1 + untax2 + atax1 + atax2,
  330. })
  331. # I click on the validate button to register the payment.
  332. context_payment = {'active_id': self.pos_order_pos3.id}
  333. self.pos_make_payment_4.with_context(context_payment).check()
  334. # I check that the order is marked as paid
  335. self.assertEqual(
  336. self.pos_order_pos3.state,
  337. 'paid',
  338. 'Order should be in paid state.'
  339. )
  340. # I test that the pickings are created as expected
  341. # One picking attached and having all the positive move lines in the correct state
  342. self.assertEqual(
  343. self.pos_order_pos3.picking_ids[0].state,
  344. 'done',
  345. 'Picking should be in done state.'
  346. )
  347. self.assertEqual(
  348. self.pos_order_pos3.picking_ids[0].move_ids.mapped('state'),
  349. ['done'],
  350. 'Move Lines should be in done state.'
  351. )
  352. # I close the session to generate the journal entries
  353. self.pos_config.current_session_id.action_pos_session_closing_control()
  354. def test_order_to_picking02(self):
  355. """ This test is similar to test_order_to_picking except that this time, there are two products:
  356. - One tracked by lot
  357. - One untracked
  358. - Both are in a sublocation of the main warehouse
  359. """
  360. tracked_product, untracked_product = self.env['product.product'].create([{
  361. 'name': 'SuperProduct Tracked',
  362. 'type': 'product',
  363. 'tracking': 'lot',
  364. 'available_in_pos': True,
  365. }, {
  366. 'name': 'SuperProduct Untracked',
  367. 'type': 'product',
  368. 'available_in_pos': True,
  369. }])
  370. wh_location = self.company_data['default_warehouse'].lot_stock_id
  371. shelf1_location = self.env['stock.location'].create({
  372. 'name': 'shelf1',
  373. 'usage': 'internal',
  374. 'location_id': wh_location.id,
  375. })
  376. lot = self.env['stock.lot'].create({
  377. 'name': 'SuperLot',
  378. 'product_id': tracked_product.id,
  379. 'company_id': self.env.company.id,
  380. })
  381. qty = 2
  382. self.env['stock.quant']._update_available_quantity(tracked_product, shelf1_location, qty, lot_id=lot)
  383. self.env['stock.quant']._update_available_quantity(untracked_product, shelf1_location, qty)
  384. self.pos_config.open_ui()
  385. self.pos_config.current_session_id.update_stock_at_closing = False
  386. untax, atax = self.compute_tax(tracked_product, 1.15, 1)
  387. for dummy in range(qty):
  388. pos_order = self.PosOrder.create({
  389. 'company_id': self.env.company.id,
  390. 'session_id': self.pos_config.current_session_id.id,
  391. 'pricelist_id': self.partner1.property_product_pricelist.id,
  392. 'partner_id': self.partner1.id,
  393. 'lines': [(0, 0, {
  394. 'name': "OL/0001",
  395. 'product_id': tracked_product.id,
  396. 'price_unit': 1.15,
  397. 'discount': 0.0,
  398. 'qty': 1.0,
  399. 'tax_ids': [(6, 0, tracked_product.taxes_id.ids)],
  400. 'price_subtotal': untax,
  401. 'price_subtotal_incl': untax + atax,
  402. 'pack_lot_ids': [[0, 0, {'lot_name': lot.name}]],
  403. }), (0, 0, {
  404. 'name': "OL/0002",
  405. 'product_id': untracked_product.id,
  406. 'price_unit': 1.15,
  407. 'discount': 0.0,
  408. 'qty': 1.0,
  409. 'tax_ids': [(6, 0, untracked_product.taxes_id.ids)],
  410. 'price_subtotal': untax,
  411. 'price_subtotal_incl': untax + atax,
  412. })],
  413. 'amount_tax': 2 * atax,
  414. 'amount_total': 2 * (untax + atax),
  415. 'amount_paid': 0,
  416. 'amount_return': 0,
  417. })
  418. context_make_payment = {
  419. "active_ids": [pos_order.id],
  420. "active_id": pos_order.id,
  421. }
  422. pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
  423. 'amount': 2 * (untax + atax),
  424. })
  425. context_payment = {'active_id': pos_order.id}
  426. pos_make_payment.with_context(context_payment).check()
  427. self.assertEqual(pos_order.state, 'paid')
  428. tracked_line = pos_order.picking_ids.move_line_ids.filtered(lambda ml: ml.product_id.id == tracked_product.id)
  429. untracked_line = pos_order.picking_ids.move_line_ids - tracked_line
  430. self.assertEqual(tracked_line.lot_id, lot)
  431. self.assertFalse(untracked_line.lot_id)
  432. self.assertEqual(tracked_line.location_id, shelf1_location)
  433. self.assertEqual(untracked_line.location_id, shelf1_location)
  434. self.pos_config.current_session_id.action_pos_session_closing_control()
  435. def test_order_to_invoice(self):
  436. self.pos_config.open_ui()
  437. current_session = self.pos_config.current_session_id
  438. untax1, atax1 = self.compute_tax(self.product3, 450*0.95, 2)
  439. untax2, atax2 = self.compute_tax(self.product4, 300*0.95, 3)
  440. # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl)
  441. self.pos_order_pos1 = self.PosOrder.create({
  442. 'company_id': self.env.company.id,
  443. 'session_id': current_session.id,
  444. 'partner_id': self.partner1.id,
  445. 'pricelist_id': self.partner1.property_product_pricelist.id,
  446. 'lines': [(0, 0, {
  447. 'name': "OL/0001",
  448. 'product_id': self.product3.id,
  449. 'price_unit': 450,
  450. 'discount': 5.0,
  451. 'qty': 2.0,
  452. 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)],
  453. 'price_subtotal': untax1,
  454. 'price_subtotal_incl': untax1 + atax1,
  455. }), (0, 0, {
  456. 'name': "OL/0002",
  457. 'product_id': self.product4.id,
  458. 'price_unit': 300,
  459. 'discount': 5.0,
  460. 'qty': 3.0,
  461. 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id.id == self.env.company.id).ids)],
  462. 'price_subtotal': untax2,
  463. 'price_subtotal_incl': untax2 + atax2,
  464. })],
  465. 'amount_tax': atax1 + atax2,
  466. 'amount_total': untax1 + untax2 + atax1 + atax2,
  467. 'amount_paid': 0.0,
  468. 'amount_return': 0.0,
  469. })
  470. # I click on the "Make Payment" wizard to pay the PoS order
  471. context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id}
  472. self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
  473. 'amount': untax1 + untax2 + atax1 + atax2,
  474. })
  475. # I click on the validate button to register the payment.
  476. context_payment = {'active_id': self.pos_order_pos1.id}
  477. self.pos_make_payment.with_context(context_payment).check()
  478. # I check that the order is marked as paid and there is no invoice
  479. # attached to it
  480. self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.")
  481. self.assertFalse(self.pos_order_pos1.account_move, 'Invoice should not be attached to order.')
  482. # I generate an invoice from the order
  483. res = self.pos_order_pos1.action_pos_order_invoice()
  484. self.assertIn('res_id', res, "Invoice should be created")
  485. # I test that the total of the attached invoice is correct
  486. invoice = self.env['account.move'].browse(res['res_id'])
  487. if invoice.state != 'posted':
  488. invoice.action_post()
  489. self.assertAlmostEqual(
  490. invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct")
  491. # I close the session to generate the journal entries
  492. current_session.action_pos_session_closing_control()
  493. def test_create_from_ui(self):
  494. """
  495. Simulation of sales coming from the interface, even after closing the session
  496. """
  497. # I click on create a new session button
  498. self.pos_config.open_ui()
  499. current_session = self.pos_config.current_session_id
  500. num_starting_orders = len(current_session.order_ids)
  501. current_session.set_cashbox_pos(0, None)
  502. untax, atax = self.compute_tax(self.led_lamp, 0.9)
  503. carrot_order = {'data':
  504. {'amount_paid': untax + atax,
  505. 'amount_return': 0,
  506. 'amount_tax': atax,
  507. 'amount_total': untax + atax,
  508. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  509. 'fiscal_position_id': False,
  510. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  511. 'lines': [[0,
  512. 0,
  513. {'discount': 0,
  514. 'pack_lot_ids': [],
  515. 'price_unit': 0.9,
  516. 'product_id': self.led_lamp.id,
  517. 'price_subtotal': 0.9,
  518. 'price_subtotal_incl': 1.04,
  519. 'qty': 1,
  520. 'tax_ids': [(6, 0, self.led_lamp.taxes_id.ids)]}]],
  521. 'name': 'Order 00042-003-0014',
  522. 'partner_id': False,
  523. 'pos_session_id': current_session.id,
  524. 'sequence_number': 2,
  525. 'statement_ids': [[0,
  526. 0,
  527. {'amount': untax + atax,
  528. 'name': fields.Datetime.now(),
  529. 'payment_method_id': self.cash_payment_method.id}]],
  530. 'uid': '00042-003-0014',
  531. 'user_id': self.env.uid},
  532. 'to_invoice': False}
  533. untax, atax = self.compute_tax(self.whiteboard_pen, 1.2)
  534. zucchini_order = {'data':
  535. {'amount_paid': untax + atax,
  536. 'amount_return': 0,
  537. 'amount_tax': atax,
  538. 'amount_total': untax + atax,
  539. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  540. 'fiscal_position_id': False,
  541. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  542. 'lines': [[0,
  543. 0,
  544. {'discount': 0,
  545. 'pack_lot_ids': [],
  546. 'price_unit': 1.2,
  547. 'product_id': self.whiteboard_pen.id,
  548. 'price_subtotal': 1.2,
  549. 'price_subtotal_incl': 1.38,
  550. 'qty': 1,
  551. 'tax_ids': [(6, 0, self.whiteboard_pen.taxes_id.ids)]}]],
  552. 'name': 'Order 00043-003-0014',
  553. 'partner_id': self.partner1.id,
  554. 'pos_session_id': current_session.id,
  555. 'sequence_number': self.pos_config.journal_id.id,
  556. 'statement_ids': [[0,
  557. 0,
  558. {'amount': untax + atax,
  559. 'name': fields.Datetime.now(),
  560. 'payment_method_id': self.credit_payment_method.id}]],
  561. 'uid': '00043-003-0014',
  562. 'user_id': self.env.uid},
  563. 'to_invoice': False}
  564. untax, atax = self.compute_tax(self.newspaper_rack, 1.28)
  565. newspaper_rack_order = {'data':
  566. {'amount_paid': untax + atax,
  567. 'amount_return': 0,
  568. 'amount_tax': atax,
  569. 'amount_total': untax + atax,
  570. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  571. 'fiscal_position_id': False,
  572. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  573. 'lines': [[0,
  574. 0,
  575. {'discount': 0,
  576. 'pack_lot_ids': [],
  577. 'price_unit': 1.28,
  578. 'product_id': self.newspaper_rack.id,
  579. 'price_subtotal': 1.28,
  580. 'price_subtotal_incl': 1.47,
  581. 'qty': 1,
  582. 'tax_ids': [[6, False, self.newspaper_rack.taxes_id.ids]]}]],
  583. 'name': 'Order 00044-003-0014',
  584. 'partner_id': False,
  585. 'pos_session_id': current_session.id,
  586. 'sequence_number': self.pos_config.journal_id.id,
  587. 'statement_ids': [[0,
  588. 0,
  589. {'amount': untax + atax,
  590. 'name': fields.Datetime.now(),
  591. 'payment_method_id': self.bank_payment_method.id}]],
  592. 'uid': '00044-003-0014',
  593. 'user_id': self.env.uid},
  594. 'to_invoice': False}
  595. # I create an order on an open session
  596. self.PosOrder.create_from_ui([carrot_order])
  597. self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded")
  598. # I close the session
  599. total_cash_payment = sum(current_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount'))
  600. current_session.post_closing_cash_details(total_cash_payment)
  601. current_session.close_session_from_ui()
  602. self.assertEqual(current_session.state, 'closed', "Session was not properly closed")
  603. self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed")
  604. # I keep selling after the session is closed
  605. with mute_logger('odoo.addons.point_of_sale.models.pos_order'):
  606. self.PosOrder.create_from_ui([zucchini_order, newspaper_rack_order])
  607. rescue_session = self.PosSession.search([
  608. ('config_id', '=', self.pos_config.id),
  609. ('state', '=', 'opened'),
  610. ('rescue', '=', True)
  611. ])
  612. self.assertEqual(len(rescue_session), 1, "One (and only one) rescue session should be created for orphan orders")
  613. self.assertIn("(RESCUE FOR %s)" % current_session.name, rescue_session.name, "Rescue session is not linked to the previous one")
  614. self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders")
  615. # I close the rescue session
  616. total_cash_payment = sum(rescue_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount'))
  617. rescue_session.post_closing_cash_details(total_cash_payment)
  618. rescue_session.close_session_from_ui()
  619. self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed")
  620. self.assertEqual(rescue_session.cash_register_balance_start, current_session.cash_register_balance_end_real, "Rescue session does not start with the same amount as the previous session")
  621. def test_order_to_payment_currency(self):
  622. """
  623. In order to test the Point of Sale in module, I will do a full flow from the sale to the payment and invoicing.
  624. I will use two products, one with price including a 10% tax, the other one with 5% tax excluded from the price.
  625. The order will be in a different currency than the company currency.
  626. """
  627. # Make sure the company is in USD
  628. self.env.ref('base.USD').active = True
  629. self.env.ref('base.EUR').active = True
  630. self.env.cr.execute(
  631. "UPDATE res_company SET currency_id = %s WHERE id = %s",
  632. [self.env.ref('base.USD').id, self.env.company.id])
  633. # Demo data are crappy, clean-up the rates
  634. self.env['res.currency.rate'].search([]).unlink()
  635. self.env['res.currency.rate'].create({
  636. 'name': '2010-01-01',
  637. 'rate': 2.0,
  638. 'currency_id': self.env.ref('base.EUR').id,
  639. })
  640. # make a config that has currency different from the company
  641. eur_pricelist = self.partner1.property_product_pricelist.copy(default={'currency_id': self.env.ref('base.EUR').id})
  642. sale_journal = self.env['account.journal'].create({
  643. 'name': 'PoS Sale EUR',
  644. 'type': 'sale',
  645. 'code': 'POSE',
  646. 'company_id': self.company.id,
  647. 'sequence': 12,
  648. 'currency_id': self.env.ref('base.EUR').id
  649. })
  650. eur_config = self.pos_config.create({
  651. 'name': 'Shop EUR Test',
  652. 'journal_id': sale_journal.id,
  653. 'use_pricelist': True,
  654. 'available_pricelist_ids': [(6, 0, eur_pricelist.ids)],
  655. 'pricelist_id': eur_pricelist.id,
  656. 'payment_method_ids': [(6, 0, self.bank_payment_method.ids)]
  657. })
  658. # I click on create a new session button
  659. eur_config.open_ui()
  660. current_session = eur_config.current_session_id
  661. # I create a PoS order with 2 units of PCSC234 at 450 EUR (Tax Incl)
  662. # and 3 units of PCSC349 at 300 EUR. (Tax Excl)
  663. untax1, atax1 = self.compute_tax(self.product3, 450, 2)
  664. untax2, atax2 = self.compute_tax(self.product4, 300, 3)
  665. self.pos_order_pos0 = self.PosOrder.create({
  666. 'company_id': self.env.company.id,
  667. 'session_id': current_session.id,
  668. 'pricelist_id': eur_pricelist.id,
  669. 'partner_id': self.partner1.id,
  670. 'lines': [(0, 0, {
  671. 'name': "OL/0001",
  672. 'product_id': self.product3.id,
  673. 'price_unit': 450,
  674. 'discount': 0.0,
  675. 'qty': 2.0,
  676. 'tax_ids': [(6, 0, self.product3.taxes_id.filtered(lambda t: t.company_id == self.env.company).ids)],
  677. 'price_subtotal': untax1,
  678. 'price_subtotal_incl': untax1 + atax1,
  679. }), (0, 0, {
  680. 'name': "OL/0002",
  681. 'product_id': self.product4.id,
  682. 'price_unit': 300,
  683. 'discount': 0.0,
  684. 'qty': 3.0,
  685. 'tax_ids': [(6, 0, self.product4.taxes_id.filtered(lambda t: t.company_id == self.env.company).ids)],
  686. 'price_subtotal': untax2,
  687. 'price_subtotal_incl': untax2 + atax2,
  688. })],
  689. 'amount_tax': atax1 + atax2,
  690. 'amount_total': untax1 + untax2 + atax1 + atax2,
  691. 'amount_paid': 0.0,
  692. 'amount_return': 0.0,
  693. })
  694. # I check that the total of the order is now equal to (450*2 +
  695. # 300*3*1.05)*0.95
  696. self.assertLess(
  697. abs(self.pos_order_pos0.amount_total - (450 * 2 + 300 * 3 * 1.05)),
  698. 0.01, 'The order has a wrong total including tax and discounts')
  699. # I click on the "Make Payment" wizard to pay the PoS order with a
  700. # partial amount of 100.0 EUR
  701. context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id}
  702. self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({
  703. 'amount': 100.0,
  704. 'payment_method_id': self.bank_payment_method.id,
  705. })
  706. # I click on the validate button to register the payment.
  707. context_payment = {'active_id': self.pos_order_pos0.id}
  708. self.pos_make_payment_0.with_context(context_payment).check()
  709. # I check that the order is not marked as paid yet
  710. self.assertEqual(self.pos_order_pos0.state, 'draft', 'Order should be in draft state.')
  711. # On the second payment proposition, I check that it proposes me the
  712. # remaining balance which is 1790.0 EUR
  713. defs = self.pos_make_payment_0.with_context({'active_id': self.pos_order_pos0.id}).default_get(['amount'])
  714. self.assertLess(
  715. abs(defs['amount'] - ((450 * 2 + 300 * 3 * 1.05) - 100.0)), 0.01, "The remaining balance is incorrect.")
  716. #'I pay the remaining balance.
  717. context_make_payment = {
  718. "active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id}
  719. self.pos_make_payment_1 = self.PosMakePayment.with_context(context_make_payment).create({
  720. 'amount': (450 * 2 + 300 * 3 * 1.05) - 100.0,
  721. 'payment_method_id': self.bank_payment_method.id,
  722. })
  723. # I click on the validate button to register the payment.
  724. self.pos_make_payment_1.with_context(context_make_payment).check()
  725. # I check that the order is marked as paid
  726. self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.')
  727. # I generate the journal entries
  728. current_session.action_pos_session_validate()
  729. # I test that the generated journal entry is attached to the PoS order
  730. self.assertTrue(current_session.move_id, "Journal entry should have been attached to the session.")
  731. # Check the amounts
  732. debit_lines = current_session.move_id.mapped('line_ids.debit')
  733. credit_lines = current_session.move_id.mapped('line_ids.credit')
  734. amount_currency_lines = current_session.move_id.mapped('line_ids.amount_currency')
  735. for a, b in zip(sorted(debit_lines), [0.0, 0.0, 0.0, 0.0, 922.5]):
  736. self.assertAlmostEqual(a, b)
  737. for a, b in zip(sorted(credit_lines), [0.0, 22.5, 40.91, 409.09, 450]):
  738. self.assertAlmostEqual(a, b)
  739. for a, b in zip(sorted(amount_currency_lines), [-900, -818.18, -81.82, -45, 1845]):
  740. self.assertAlmostEqual(a, b)
  741. def test_order_to_invoice_no_tax(self):
  742. self.pos_config.open_ui()
  743. current_session = self.pos_config.current_session_id
  744. # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl)
  745. self.pos_order_pos1 = self.PosOrder.create({
  746. 'company_id': self.env.company.id,
  747. 'session_id': current_session.id,
  748. 'partner_id': self.partner1.id,
  749. 'pricelist_id': self.partner1.property_product_pricelist.id,
  750. 'lines': [(0, 0, {
  751. 'name': "OL/0001",
  752. 'product_id': self.product3.id,
  753. 'price_unit': 450,
  754. 'discount': 5.0,
  755. 'qty': 2.0,
  756. 'price_subtotal': 855,
  757. 'price_subtotal_incl': 855,
  758. }), (0, 0, {
  759. 'name': "OL/0002",
  760. 'product_id': self.product4.id,
  761. 'price_unit': 300,
  762. 'discount': 5.0,
  763. 'qty': 3.0,
  764. 'price_subtotal': 855,
  765. 'price_subtotal_incl': 855,
  766. })],
  767. 'amount_tax': 855 * 2,
  768. 'amount_total': 855 * 2,
  769. 'amount_paid': 0.0,
  770. 'amount_return': 0.0,
  771. })
  772. # I click on the "Make Payment" wizard to pay the PoS order
  773. context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id}
  774. self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
  775. 'amount': 855 * 2,
  776. })
  777. # I click on the validate button to register the payment.
  778. context_payment = {'active_id': self.pos_order_pos1.id}
  779. self.pos_make_payment.with_context(context_payment).check()
  780. # I check that the order is marked as paid and there is no invoice
  781. # attached to it
  782. self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.")
  783. self.assertFalse(self.pos_order_pos1.account_move, 'Invoice should not be attached to order yet.')
  784. # I generate an invoice from the order
  785. res = self.pos_order_pos1.action_pos_order_invoice()
  786. self.assertIn('res_id', res, "No invoice created")
  787. # I test that the total of the attached invoice is correct
  788. invoice = self.env['account.move'].browse(res['res_id'])
  789. if invoice.state != 'posted':
  790. invoice.action_post()
  791. self.assertAlmostEqual(
  792. invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct")
  793. for iline in invoice.invoice_line_ids:
  794. self.assertFalse(iline.tax_ids)
  795. self.pos_config.current_session_id.action_pos_session_closing_control()
  796. def test_order_with_deleted_tax(self):
  797. # create tax
  798. dummy_50_perc_tax = self.env['account.tax'].create({
  799. 'name': 'Tax 50%',
  800. 'amount_type': 'percent',
  801. 'amount': 50.0,
  802. 'price_include': 0
  803. })
  804. # set tax to product
  805. product5 = self.env['product.product'].create({
  806. 'name': 'product5',
  807. 'type': 'product',
  808. 'categ_id': self.env.ref('product.product_category_all').id,
  809. 'taxes_id': dummy_50_perc_tax.ids
  810. })
  811. # sell product thru pos
  812. self.pos_config.open_ui()
  813. pos_session = self.pos_config.current_session_id
  814. untax, atax = self.compute_tax(product5, 10.0)
  815. product5_order = {'data':
  816. {'amount_paid': untax + atax,
  817. 'amount_return': 0,
  818. 'amount_tax': atax,
  819. 'amount_total': untax + atax,
  820. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  821. 'fiscal_position_id': False,
  822. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  823. 'lines': [[0,
  824. 0,
  825. {'discount': 0,
  826. 'pack_lot_ids': [],
  827. 'price_unit': 10.0,
  828. 'product_id': product5.id,
  829. 'price_subtotal': 10.0,
  830. 'price_subtotal_incl': 15.0,
  831. 'qty': 1,
  832. 'tax_ids': [(6, 0, product5.taxes_id.ids)]}]],
  833. 'name': 'Order 12345-123-1234',
  834. 'partner_id': False,
  835. 'pos_session_id': pos_session.id,
  836. 'sequence_number': 2,
  837. 'statement_ids': [[0,
  838. 0,
  839. {'amount': untax + atax,
  840. 'name': fields.Datetime.now(),
  841. 'payment_method_id': self.cash_payment_method.id}]],
  842. 'uid': '12345-123-1234',
  843. 'user_id': self.env.uid},
  844. 'to_invoice': False}
  845. self.PosOrder.create_from_ui([product5_order])
  846. # delete tax
  847. dummy_50_perc_tax.unlink()
  848. total_cash_payment = sum(pos_session.mapped('order_ids.payment_ids').filtered(lambda payment: payment.payment_method_id.type == 'cash').mapped('amount'))
  849. pos_session.post_closing_cash_details(total_cash_payment)
  850. # close session (should not fail here)
  851. # We don't call `action_pos_session_closing_control` to force the failed
  852. # closing which will return the action because the internal rollback call messes
  853. # with the rollback of the test runner. So instead, we directly call the method
  854. # that returns the action by specifying the imbalance amount.
  855. action = pos_session._close_session_action(5.0)
  856. wizard = self.env['pos.close.session.wizard'].browse(action['res_id'])
  857. wizard.with_context(action['context']).close_session()
  858. # check the difference line
  859. diff_line = pos_session.move_id.line_ids.filtered(lambda line: line.name == 'Difference at closing PoS session')
  860. self.assertAlmostEqual(diff_line.credit, 5.0, msg="Missing amount of 5.0")
  861. def test_order_multi_step_route(self):
  862. """ Test that orders in sessions with "Ship Later" enabled and "Specific Route" set to a
  863. multi-step (2/3) route can be validated. This config implies multiple picking types
  864. and multiple move_lines.
  865. """
  866. tracked_product = self.env['product.product'].create({
  867. 'name': 'SuperProduct Tracked',
  868. 'type': 'product',
  869. 'tracking': 'lot',
  870. 'available_in_pos': True
  871. })
  872. tracked_product_2 = self.env['product.product'].create({
  873. 'name': 'SuperProduct Tracked 2',
  874. 'type': 'product',
  875. 'tracking': 'lot',
  876. 'available_in_pos': True
  877. })
  878. tracked_product_2_lot = self.env['stock.lot'].create({
  879. 'name': '80085',
  880. 'product_id': tracked_product_2.id,
  881. 'company_id': self.env.company.id,
  882. })
  883. stock_location = self.company_data['default_warehouse'].lot_stock_id
  884. self.env['stock.quant'].with_context(inventory_mode=True).create({
  885. 'product_id': tracked_product_2.id,
  886. 'inventory_quantity': 1,
  887. 'location_id': stock_location.id,
  888. 'lot_id': tracked_product_2_lot.id
  889. }).action_apply_inventory()
  890. warehouse_id = self.company_data['default_warehouse']
  891. warehouse_id.delivery_steps = 'pick_ship'
  892. self.pos_config.ship_later = True
  893. self.pos_config.warehouse_id = warehouse_id
  894. self.pos_config.route_id = warehouse_id.route_ids[-1]
  895. self.pos_config.open_ui()
  896. self.pos_config.current_session_id.update_stock_at_closing = False
  897. untax, tax = self.compute_tax(tracked_product, 1.15, 1)
  898. pos_order = self.PosOrder.create({
  899. 'company_id': self.env.company.id,
  900. 'session_id': self.pos_config.current_session_id.id,
  901. 'pricelist_id': self.partner1.property_product_pricelist.id,
  902. 'partner_id': self.partner1.id,
  903. 'lines': [(0, 0, {
  904. 'name': "OL/0001",
  905. 'product_id': tracked_product.id,
  906. 'price_unit': 1.15,
  907. 'qty': 1.0,
  908. 'price_subtotal': untax,
  909. 'price_subtotal_incl': untax + tax,
  910. 'pack_lot_ids': [
  911. [0, 0, {'lot_name': '80085'}],
  912. ]
  913. }),
  914. (0, 0, {
  915. 'name': "OL/0002",
  916. 'product_id': tracked_product_2.id,
  917. 'price_unit': 1.15,
  918. 'qty': 1.0,
  919. 'price_subtotal': untax,
  920. 'price_subtotal_incl': untax + tax,
  921. 'pack_lot_ids': [
  922. [0, 0, {'lot_name': '80085'}],
  923. ]
  924. })],
  925. 'amount_tax': tax,
  926. 'amount_total': untax+tax,
  927. 'amount_paid': 0,
  928. 'amount_return': 0,
  929. 'to_ship': True,
  930. })
  931. context_make_payment = {
  932. "active_ids": [pos_order.id],
  933. "active_id": pos_order.id,
  934. }
  935. pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
  936. 'amount': untax+tax,
  937. })
  938. context_payment = {'active_id': pos_order.id}
  939. pos_make_payment.with_context(context_payment).check()
  940. pickings = pos_order.picking_ids
  941. picking_mls_no_stock = pickings.move_line_ids.filtered(lambda l: l.product_id.id == tracked_product.id)
  942. picking_mls_stock = pickings.move_line_ids.filtered(lambda l: l.product_id.id == tracked_product_2.id)
  943. self.assertEqual(pos_order.state, 'paid')
  944. self.assertEqual(len(picking_mls_no_stock), 0)
  945. self.assertEqual(len(picking_mls_stock), 1)
  946. self.assertEqual(len(pickings.picking_type_id), 2)
  947. def test_order_refund_picking(self):
  948. self.pos_config.open_ui()
  949. current_session = self.pos_config.current_session_id
  950. current_session.update_stock_at_closing = True
  951. # I create a new PoS order with 1 line
  952. order = self.PosOrder.create({
  953. 'company_id': self.env.company.id,
  954. 'session_id': current_session.id,
  955. 'partner_id': self.partner1.id,
  956. 'pricelist_id': self.partner1.property_product_pricelist.id,
  957. 'lines': [(0, 0, {
  958. 'name': "OL/0001",
  959. 'product_id': self.product3.id,
  960. 'price_unit': 450,
  961. 'discount': 5.0,
  962. 'qty': 2.0,
  963. 'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
  964. 'price_subtotal': 450 * (1 - 5/100.0) * 2,
  965. 'price_subtotal_incl': 450 * (1 - 5/100.0) * 2,
  966. })],
  967. 'amount_total': 1710.0,
  968. 'amount_tax': 0.0,
  969. 'amount_paid': 0.0,
  970. 'amount_return': 0.0,
  971. 'to_invoice': True
  972. })
  973. payment_context = {"active_ids": order.ids, "active_id": order.id}
  974. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  975. 'amount': order.amount_total,
  976. 'payment_method_id': self.cash_payment_method.id
  977. })
  978. order_payment.with_context(**payment_context).check()
  979. # I create a refund
  980. refund_action = order.refund()
  981. refund = self.PosOrder.browse(refund_action['res_id'])
  982. payment_context = {"active_ids": refund.ids, "active_id": refund.id}
  983. refund_payment = self.PosMakePayment.with_context(**payment_context).create({
  984. 'amount': refund.amount_total,
  985. 'payment_method_id': self.cash_payment_method.id,
  986. })
  987. # I click on the validate button to register the payment.
  988. refund_payment.with_context(**payment_context).check()
  989. refund.action_pos_order_invoice()
  990. self.assertEqual(refund.picking_count, 1)
  991. def test_order_with_different_payments_and_refund(self):
  992. """
  993. Test that all the payments are correctly taken into account when the order contains multiple payments and money refund.
  994. In this example, we create an order with two payments for a product of 750$:
  995. - one payment of $300 with customer account
  996. - one payment of $460 with cash
  997. Then, we refund the order with $10, and check that the amount still due is 300$.
  998. """
  999. product5 = self.env['product.product'].create({
  1000. 'name': 'product5',
  1001. 'type': 'product',
  1002. 'categ_id': self.env.ref('product.product_category_all').id,
  1003. })
  1004. # sell product thru pos
  1005. self.pos_config.open_ui()
  1006. pos_session = self.pos_config.current_session_id
  1007. product5_order = {'data':
  1008. {'amount_paid': 750,
  1009. 'amount_return': 10,
  1010. 'amount_tax': 0,
  1011. 'amount_total': 750,
  1012. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  1013. 'fiscal_position_id': False,
  1014. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  1015. 'lines': [[0, 0, {
  1016. 'discount': 0,
  1017. 'pack_lot_ids': [],
  1018. 'price_unit': 750.0,
  1019. 'product_id': product5.id,
  1020. 'price_subtotal': 750.0,
  1021. 'price_subtotal_incl': 750.0,
  1022. 'tax_ids': [[6, False, []]],
  1023. 'qty': 1,
  1024. }]],
  1025. 'name': 'Order 12345-123-1234',
  1026. 'partner_id': self.partner1.id,
  1027. 'pos_session_id': pos_session.id,
  1028. 'sequence_number': 2,
  1029. 'statement_ids': [[0, 0, {
  1030. 'amount': 460,
  1031. 'name': fields.Datetime.now(),
  1032. 'payment_method_id': self.cash_payment_method.id
  1033. }], [0, 0, {
  1034. 'amount': 300,
  1035. 'name': fields.Datetime.now(),
  1036. 'payment_method_id': self.credit_payment_method.id
  1037. }]],
  1038. 'uid': '12345-123-1234',
  1039. 'user_id': self.env.uid,
  1040. 'to_invoice': True, }
  1041. }
  1042. pos_order_id = self.PosOrder.create_from_ui([product5_order])[0]['id']
  1043. pos_order = self.PosOrder.search([('id', '=', pos_order_id)])
  1044. #assert account_move amount_residual is 300
  1045. self.assertEqual(pos_order.account_move.amount_residual, 300)
  1046. def test_sale_order_postponed_invoicing(self):
  1047. """ Test the flow of creating an invoice later, after the POS session has been closed and everything has been processed.
  1048. The process should:
  1049. - Create a new misc entry, that will revert part of the POS closing entry.
  1050. - Create the move and associating payment(s) entry, as it would do when closing with invoice.
  1051. - Reconcile the receivable lines from the created misc entry with the ones from the created payment(s)
  1052. """
  1053. # Create the order on the first of january.
  1054. with freeze_time('2020-01-01'):
  1055. product = self.env['product.product'].create({
  1056. 'name': 'Dummy product',
  1057. 'type': 'product',
  1058. 'categ_id': self.env.ref('product.product_category_all').id,
  1059. 'taxes_id': self.tax_sale_a.ids,
  1060. })
  1061. self.pos_config.open_ui()
  1062. pos_session = self.pos_config.current_session_id
  1063. untax, atax = self.compute_tax(product, 500, 1)
  1064. pos_order_data = {
  1065. 'data': {
  1066. 'amount_paid': untax + atax,
  1067. 'amount_return': 0,
  1068. 'amount_tax': atax,
  1069. 'amount_total': untax + atax,
  1070. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  1071. 'fiscal_position_id': False,
  1072. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  1073. 'lines': [(0, 0, {
  1074. 'discount': 0,
  1075. 'id': 42,
  1076. 'pack_lot_ids': [],
  1077. 'price_unit': 500.0,
  1078. 'product_id': product.id,
  1079. 'price_subtotal': 500.0,
  1080. 'price_subtotal_incl': 575.0,
  1081. 'qty': 1,
  1082. 'tax_ids': [(6, 0, product.taxes_id.ids)]
  1083. })],
  1084. 'name': 'Order 12345-123-1234',
  1085. 'partner_id': False,
  1086. 'pos_session_id': pos_session.id,
  1087. 'sequence_number': 2,
  1088. 'statement_ids': [(0, 0, {
  1089. 'amount': untax + atax,
  1090. 'name': fields.Datetime.now(),
  1091. 'payment_method_id': self.cash_payment_method.id
  1092. })],
  1093. 'uid': '12345-123-1234',
  1094. 'user_id': self.env.uid
  1095. },
  1096. 'id': '12345-123-1234',
  1097. 'to_invoice': False
  1098. }
  1099. pos_order_id = self.PosOrder.create_from_ui([pos_order_data])[0]['id']
  1100. pos_order = self.env['pos.order'].browse(pos_order_id)
  1101. # End the session. The order has been created without any invoice.
  1102. self.pos_config.current_session_id.action_pos_session_closing_control()
  1103. self.assertFalse(pos_order.account_move.exists())
  1104. # Client is back on the 3rd, asks for an invoice.
  1105. with freeze_time('2020-01-03'):
  1106. # We set the partner on the order
  1107. pos_order.partner_id = self.partner1.id
  1108. pos_order.action_pos_order_invoice()
  1109. # We should now have: an invoice, a payment, and a misc entry reconciled with the payment that reverse the original POS closing entry.
  1110. invoice = pos_order.account_move
  1111. closing_entry = pos_order.session_move_id
  1112. # This search isn't the best, but we don't have any references to this move stored on other models.
  1113. misc_reversal_entry = self.env['account.move'].search([('ref', '=', f'Reversal of POS closing entry {closing_entry.name} for order {pos_order.name} from session {pos_order.session_id.name}')])
  1114. # In this case we will have only one, for cash payment
  1115. payment = self.env['account.move'].search([('ref', '=like', f'Invoice payment for {pos_order.name} ({pos_order.account_move.name}) using {self.cash_payment_method.name}')])
  1116. # And thus only one bank statement for it
  1117. statement = self.env['account.move'].search([('journal_id', '=', self.company_data['default_journal_cash'].id)])
  1118. self.assertTrue(invoice.exists() and closing_entry.exists() and misc_reversal_entry.exists() and payment.exists())
  1119. # Check 1: Check that we have reversed every credit line on the closing entry.
  1120. for closing_entry_line, misc_reversal_entry_line in zip(closing_entry.line_ids, misc_reversal_entry.line_ids):
  1121. if closing_entry_line.balance < 0:
  1122. self.assertEqual(closing_entry_line.balance, -misc_reversal_entry_line.balance)
  1123. self.assertEqual(closing_entry_line.account_id, misc_reversal_entry_line.account_id)
  1124. # Check 2: Reconciliation
  1125. # The invoice receivable should be reconciled with the payment receivable of the same account.
  1126. invoice_receivable_line = invoice.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable'])
  1127. payment_receivable_line = payment.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable'])
  1128. self.assertEqual(invoice_receivable_line.matching_number, payment_receivable_line.matching_number)
  1129. # The payment receivable (POS) is reconciled with the closing entry receivable (POS)
  1130. payment_receivable_pos_line = payment.line_ids.filtered(lambda line: line.account_id == self.company_data['company'].account_default_pos_receivable_account_id)
  1131. misc_receivable_pos_line = misc_reversal_entry.line_ids.filtered(lambda line: line.account_id == self.company_data['company'].account_default_pos_receivable_account_id)
  1132. self.assertEqual(misc_receivable_pos_line.matching_number, payment_receivable_pos_line.matching_number)
  1133. # The closing entry receivable is reconciled with the bank statement
  1134. closing_entry_receivable_line = closing_entry.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable']) # Because the payment method use the default receivable
  1135. statement_receivable_line = statement.line_ids.filtered(lambda line: line.account_id == self.company_data['default_account_receivable'] and line.name == pos_order.session_id.name) # Because the payment method use the default receivable
  1136. self.assertEqual(closing_entry_receivable_line.matching_number, statement_receivable_line.matching_number)
  1137. def test_order_pos_tax_same_as_company(self):
  1138. """Test that when the default_pos_receivable_account and the partner account_receivable are the same,
  1139. payment are correctly reconciled and the invoice is correctly marked as paid.
  1140. """
  1141. self.pos_config.open_ui()
  1142. current_session = self.pos_config.current_session_id
  1143. current_session.company_id.account_default_pos_receivable_account_id = self.partner1.property_account_receivable_id
  1144. product5_order = {'data':
  1145. {'amount_paid': 750,
  1146. 'amount_tax': 0,
  1147. 'amount_return':0,
  1148. 'amount_total': 750,
  1149. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  1150. 'fiscal_position_id': False,
  1151. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  1152. 'lines': [[0, 0, {
  1153. 'discount': 0,
  1154. 'pack_lot_ids': [],
  1155. 'price_unit': 750.0,
  1156. 'product_id': self.product3.id,
  1157. 'price_subtotal': 750.0,
  1158. 'price_subtotal_incl': 750.0,
  1159. 'tax_ids': [[6, False, []]],
  1160. 'qty': 1,
  1161. }]],
  1162. 'name': 'Order 12345-123-1234',
  1163. 'partner_id': self.partner1.id,
  1164. 'pos_session_id': current_session.id,
  1165. 'sequence_number': 2,
  1166. 'statement_ids': [[0, 0, {
  1167. 'amount': 450,
  1168. 'name': fields.Datetime.now(),
  1169. 'payment_method_id': self.cash_payment_method.id
  1170. }], [0, 0, {
  1171. 'amount': 300,
  1172. 'name': fields.Datetime.now(),
  1173. 'payment_method_id': self.bank_payment_method.id
  1174. }]],
  1175. 'uid': '12345-123-1234',
  1176. 'user_id': self.env.uid,
  1177. 'to_invoice': True, }
  1178. }
  1179. pos_order_id = self.PosOrder.create_from_ui([product5_order])[0]['id']
  1180. pos_order = self.PosOrder.search([('id', '=', pos_order_id)])
  1181. self.assertEqual(pos_order.account_move.amount_residual, 0)
  1182. def test_order_refund_with_owner(self):
  1183. # open pos session
  1184. self.pos_config.open_ui()
  1185. current_session = self.pos_config.current_session_id
  1186. # set up product iwith SN tracing and create two lots (1001, 1002)
  1187. self.stock_location = self.company_data['default_warehouse'].lot_stock_id
  1188. self.product2 = self.env['product.product'].create({
  1189. 'name': 'Product A',
  1190. 'type': 'product',
  1191. 'categ_id': self.env.ref('product.product_category_all').id,
  1192. })
  1193. self.env['stock.quant'].with_context(inventory_mode=True).create({
  1194. 'product_id': self.product2.id,
  1195. 'inventory_quantity': 1,
  1196. 'location_id': self.stock_location.id,
  1197. 'owner_id': self.partner1.id
  1198. }).action_apply_inventory()
  1199. # create pos order with the two SN created before
  1200. order = self.PosOrder.create({
  1201. 'company_id': self.env.company.id,
  1202. 'session_id': current_session.id,
  1203. 'partner_id': self.partner1.id,
  1204. 'lines': [(0, 0, {
  1205. 'name': "OL/0001",
  1206. 'product_id': self.product2.id,
  1207. 'price_unit': 6,
  1208. 'discount': 0,
  1209. 'qty': 1,
  1210. 'tax_ids': [[6, False, []]],
  1211. 'price_subtotal': 6,
  1212. 'price_subtotal_incl': 6,
  1213. })],
  1214. 'pricelist_id': self.pos_config.pricelist_id.id,
  1215. 'amount_paid': 6.0,
  1216. 'amount_total': 6.0,
  1217. 'amount_tax': 0.0,
  1218. 'amount_return': 0.0,
  1219. 'to_invoice': False,
  1220. })
  1221. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1222. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1223. 'amount': order.amount_total,
  1224. 'payment_method_id': self.cash_payment_method.id
  1225. })
  1226. order_payment.with_context(**payment_context).check()
  1227. # I create a refund
  1228. refund_action = order.refund()
  1229. refund = self.PosOrder.browse(refund_action['res_id'])
  1230. payment_context = {"active_ids": refund.ids, "active_id": refund.id}
  1231. refund_payment = self.PosMakePayment.with_context(**payment_context).create({
  1232. 'amount': refund.amount_total,
  1233. 'payment_method_id': self.cash_payment_method.id,
  1234. })
  1235. # I click on the validate button to register the payment.
  1236. refund_payment.with_context(**payment_context).check()
  1237. current_session.action_pos_session_closing_control()
  1238. self.assertEqual(refund.picking_ids.move_line_ids_without_package.owner_id.id, order.picking_ids.move_line_ids_without_package.owner_id.id, "The owner of the refund is not the same as the owner of the original order")
  1239. def test_journal_entries_category_without_account(self):
  1240. #create a new product category without account
  1241. category = self.env['product.category'].create({
  1242. 'name': 'Category without account',
  1243. 'property_account_income_categ_id': False,
  1244. 'property_account_expense_categ_id': False,
  1245. })
  1246. account = self.env['account.account'].create({
  1247. 'name': 'Account for category without account',
  1248. 'code': 'X1111',
  1249. })
  1250. product = self.env['product.product'].create({
  1251. 'name': 'Product with category without account',
  1252. 'type': 'product',
  1253. 'categ_id': category.id,
  1254. 'property_account_income_id': account,
  1255. })
  1256. self.pos_config.journal_id.default_account_id = account.id
  1257. #create a new pos order with the product
  1258. self.pos_config.open_ui()
  1259. current_session = self.pos_config.current_session_id
  1260. order = self.PosOrder.create({
  1261. 'company_id': self.env.company.id,
  1262. 'session_id': current_session.id,
  1263. 'partner_id': self.partner1.id,
  1264. 'pricelist_id': self.partner1.property_product_pricelist.id,
  1265. 'lines': [(0, 0, {
  1266. 'name': "OL/0001",
  1267. 'product_id': product.id,
  1268. 'price_unit': 10,
  1269. 'discount': 0.0,
  1270. 'qty': 1,
  1271. 'tax_ids': [],
  1272. 'price_subtotal': 10,
  1273. 'price_subtotal_incl': 10,
  1274. })],
  1275. 'amount_total': 10,
  1276. 'amount_tax': 0.0,
  1277. 'amount_paid': 10,
  1278. 'amount_return': 0.0,
  1279. 'to_invoice': True
  1280. })
  1281. #create a payment
  1282. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1283. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1284. 'amount': order.amount_total,
  1285. 'payment_method_id': self.cash_payment_method.id
  1286. })
  1287. order_payment.with_context(**payment_context).check()
  1288. current_session.action_pos_session_closing_control()
  1289. self.assertEqual(current_session.move_id.line_ids[0].account_id.id, account.id)
  1290. def test_tracked_product_with_owner(self):
  1291. # open pos session
  1292. self.pos_config.open_ui()
  1293. current_session = self.pos_config.current_session_id
  1294. # set up product iwith SN tracing and create two lots (1001, 1002)
  1295. self.stock_location = self.company_data['default_warehouse'].lot_stock_id
  1296. self.product2 = self.env['product.product'].create({
  1297. 'name': 'Product A',
  1298. 'type': 'product',
  1299. 'tracking': 'serial',
  1300. 'categ_id': self.env.ref('product.product_category_all').id,
  1301. })
  1302. lot1 = self.env['stock.lot'].create({
  1303. 'name': '1001',
  1304. 'product_id': self.product2.id,
  1305. 'company_id': self.env.company.id,
  1306. })
  1307. self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1, owner_id=self.partner1)
  1308. # create pos order with the two SN created before
  1309. order = self.PosOrder.create({
  1310. 'company_id': self.env.company.id,
  1311. 'session_id': current_session.id,
  1312. 'partner_id': self.partner1.id,
  1313. 'lines': [(0, 0, {
  1314. 'name': "OL/0001",
  1315. 'id': 1,
  1316. 'product_id': self.product2.id,
  1317. 'price_unit': 6,
  1318. 'discount': 0,
  1319. 'qty': 1,
  1320. 'tax_ids': [[6, False, []]],
  1321. 'price_subtotal': 6,
  1322. 'price_subtotal_incl': 6,
  1323. 'pack_lot_ids': [
  1324. [0, 0, {'lot_name': '1001'}],
  1325. ]
  1326. })],
  1327. 'pricelist_id': self.pos_config.pricelist_id.id,
  1328. 'amount_paid': 6.0,
  1329. 'amount_total': 6.0,
  1330. 'amount_tax': 0.0,
  1331. 'amount_return': 0.0,
  1332. 'to_invoice': False,
  1333. })
  1334. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1335. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1336. 'amount': order.amount_total,
  1337. 'payment_method_id': self.cash_payment_method.id
  1338. })
  1339. order_payment.with_context(**payment_context).check()
  1340. current_session.action_pos_session_closing_control()
  1341. self.assertEqual(current_session.picking_ids.move_line_ids.owner_id.id, self.partner1.id)
  1342. def test_order_refund_with_invoice(self):
  1343. """This test make sure that credit notes of pos orders are correctly
  1344. linked to the original invoice."""
  1345. self.pos_config.open_ui()
  1346. current_session = self.pos_config.current_session_id
  1347. order_data = {'data':
  1348. {'amount_paid': 450,
  1349. 'amount_tax': 0,
  1350. 'amount_return': 0,
  1351. 'amount_total': 450,
  1352. 'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
  1353. 'fiscal_position_id': False,
  1354. 'pricelist_id': self.pos_config.available_pricelist_ids[0].id,
  1355. 'lines': [[0, 0, {
  1356. 'discount': 0,
  1357. 'pack_lot_ids': [],
  1358. 'price_unit': 450.0,
  1359. 'product_id': self.product3.id,
  1360. 'price_subtotal': 450.0,
  1361. 'price_subtotal_incl': 450.0,
  1362. 'tax_ids': [[6, False, []]],
  1363. 'qty': 1,
  1364. }]],
  1365. 'name': 'Order 12345-123-1234',
  1366. 'partner_id': self.partner1.id,
  1367. 'pos_session_id': current_session.id,
  1368. 'sequence_number': 2,
  1369. 'statement_ids': [[0, 0, {
  1370. 'amount': 450,
  1371. 'name': fields.Datetime.now(),
  1372. 'payment_method_id': self.cash_payment_method.id
  1373. }]],
  1374. 'uid': '12345-123-1234',
  1375. 'user_id': self.env.uid,
  1376. 'to_invoice': True, }
  1377. }
  1378. order = self.PosOrder.create_from_ui([order_data])
  1379. order = self.PosOrder.browse(order[0]['id'])
  1380. refund_id = order.refund()['res_id']
  1381. refund = self.PosOrder.browse(refund_id)
  1382. context_payment = {"active_ids": refund.ids, "active_id": refund.id}
  1383. refund_payment = self.PosMakePayment.with_context(**context_payment).create({
  1384. 'amount': refund.amount_total,
  1385. 'payment_method_id': self.cash_payment_method.id
  1386. })
  1387. refund_payment.with_context(**context_payment).check()
  1388. refund.action_pos_order_invoice()
  1389. #get last invoice created
  1390. current_session.action_pos_session_closing_control()
  1391. invoices = self.env['account.move'].search([('move_type', '=', 'out_invoice')], order='id desc', limit=1)
  1392. credit_notes = self.env['account.move'].search([('move_type', '=', 'out_refund')], order='id desc', limit=1)
  1393. self.assertEqual(credit_notes.ref, "Reversal of: "+invoices.name)
  1394. self.assertEqual(credit_notes.reversed_entry_id.id, invoices.id)
  1395. def test_invoicing_after_closing_session(self):
  1396. """ Test that an invoice can be created after the session is closed """
  1397. #create customer account payment method
  1398. self.customer_account_payment_method = self.env['pos.payment.method'].create({
  1399. 'name': 'Customer Account',
  1400. 'split_transactions': True,
  1401. })
  1402. self.product1 = self.env['product.product'].create({
  1403. 'name': 'Product A',
  1404. 'type': 'product',
  1405. 'categ_id': self.env.ref('product.product_category_all').id,
  1406. })
  1407. self.partner1.write({'parent_id': self.env['res.partner'].create({'name': 'Parent'}).id})
  1408. #add customer account payment method to pos config
  1409. self.pos_config.write({
  1410. 'payment_method_ids': [(4, self.customer_account_payment_method.id, 0)],
  1411. })
  1412. # change the currency of PoS config
  1413. (self.currency_data['currency'].rate_ids | self.company.currency_id.rate_ids).unlink()
  1414. self.env['res.currency.rate'].create({
  1415. 'rate': 0.5,
  1416. 'currency_id': self.currency_data['currency'].id,
  1417. 'name': datetime.today().date(),
  1418. })
  1419. self.pos_config.journal_id.write({
  1420. 'currency_id': self.currency_data['currency'].id
  1421. })
  1422. other_pricelist = self.env['product.pricelist'].create({
  1423. 'name': 'Public Pricelist Other',
  1424. 'currency_id': self.currency_data['currency'].id,
  1425. })
  1426. self.pos_config.write({
  1427. 'pricelist_id': other_pricelist.id,
  1428. 'available_pricelist_ids': [(6, 0, other_pricelist.ids)],
  1429. })
  1430. self.pos_config.open_ui()
  1431. current_session = self.pos_config.current_session_id
  1432. # create pos order
  1433. order = self.PosOrder.create({
  1434. 'company_id': self.env.company.id,
  1435. 'session_id': current_session.id,
  1436. 'partner_id': self.partner1.id,
  1437. 'lines': [(0, 0, {
  1438. 'name': "OL/0001",
  1439. 'product_id': self.product1.id,
  1440. 'price_unit': 6,
  1441. 'discount': 0,
  1442. 'qty': 1,
  1443. 'tax_ids': [[6, False, []]],
  1444. 'price_subtotal': 6,
  1445. 'price_subtotal_incl': 6,
  1446. })],
  1447. 'pricelist_id': self.pos_config.pricelist_id.id,
  1448. 'amount_paid': 6.0,
  1449. 'amount_total': 6.0,
  1450. 'amount_tax': 0.0,
  1451. 'amount_return': 0.0,
  1452. 'to_invoice': True,
  1453. })
  1454. #pay for the order with customer account
  1455. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1456. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1457. 'amount': 2.0,
  1458. 'payment_method_id': self.cash_payment_method.id
  1459. })
  1460. order_payment.with_context(**payment_context).check()
  1461. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1462. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1463. 'amount': 4.0,
  1464. 'payment_method_id': self.customer_account_payment_method.id
  1465. })
  1466. order_payment.with_context(**payment_context).check()
  1467. # close session
  1468. current_session.action_pos_session_closing_control()
  1469. # create invoice
  1470. order.action_pos_order_invoice()
  1471. #get journal entry that does the reverse payment, it the ref must contains Reversal
  1472. reverse_payment = self.env['account.move'].search([('ref', 'ilike', "Reversal")])
  1473. original_payment = self.env['account.move'].search([('ref', '=', current_session.display_name)])
  1474. original_customer_payment_entry = original_payment.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable')
  1475. reverser_customer_payment_entry = reverse_payment.line_ids.filtered(lambda l: l.account_id.account_type == 'asset_receivable')
  1476. #check that both use the same account
  1477. self.assertEqual(len(reverser_customer_payment_entry), 2)
  1478. self.assertTrue(order.account_move.line_ids.partner_id == self.partner1.commercial_partner_id)
  1479. self.assertEqual(reverser_customer_payment_entry[0].balance, -4.0)
  1480. self.assertEqual(reverser_customer_payment_entry[1].balance, -8.0)
  1481. self.assertEqual(reverser_customer_payment_entry[0].amount_currency, -2.0)
  1482. self.assertEqual(reverser_customer_payment_entry[1].amount_currency, -4.0)
  1483. self.assertEqual(original_customer_payment_entry.account_id.id, reverser_customer_payment_entry.account_id.id)
  1484. self.assertEqual(reverser_customer_payment_entry.partner_id, original_customer_payment_entry.partner_id)
  1485. def test_order_total_subtotal_account_line_values(self):
  1486. self.tax1 = self.env['account.tax'].create({
  1487. 'name': 'Tax 1',
  1488. 'amount': 10,
  1489. 'amount_type': 'percent',
  1490. 'type_tax_use': 'sale',
  1491. })
  1492. #create an account to be used as income account
  1493. self.account1 = self.env['account.account'].create({
  1494. 'name': 'Account 1',
  1495. 'code': 'AC1',
  1496. 'account_type': 'income',
  1497. 'reconcile': True,
  1498. })
  1499. self.product1 = self.env['product.product'].create({
  1500. 'name': 'Product A',
  1501. 'type': 'product',
  1502. 'taxes_id': [(6, 0, self.tax1.ids)],
  1503. 'categ_id': self.env.ref('product.product_category_all').id,
  1504. 'property_account_income_id': self.account1.id,
  1505. })
  1506. self.product2 = self.env['product.product'].create({
  1507. 'name': 'Product B',
  1508. 'type': 'product',
  1509. 'taxes_id': [(6, 0, self.tax1.ids)],
  1510. 'categ_id': self.env.ref('product.product_category_all').id,
  1511. 'property_account_income_id': self.account1.id,
  1512. })
  1513. self.pos_config.open_ui()
  1514. #create an order with product1
  1515. order = self.PosOrder.create({
  1516. 'company_id': self.env.company.id,
  1517. 'session_id': self.pos_config.current_session_id.id,
  1518. 'partner_id': self.partner1.id,
  1519. 'lines': [(0, 0, {
  1520. 'name': "OL/0001",
  1521. 'product_id': self.product1.id,
  1522. 'price_unit': 100,
  1523. 'discount': 0,
  1524. 'qty': 1,
  1525. 'tax_ids': [[6, False, [self.tax1.id]]],
  1526. 'price_subtotal': 100,
  1527. 'price_subtotal_incl': 110,
  1528. }), (0, 0, {
  1529. 'name': "OL/0002",
  1530. 'product_id': self.product2.id,
  1531. 'price_unit': 100,
  1532. 'discount': 0,
  1533. 'qty': 1,
  1534. 'tax_ids': [[6, False, [self.tax1.id]]],
  1535. 'price_subtotal': 100,
  1536. 'price_subtotal_incl': 110,
  1537. })],
  1538. 'pricelist_id': self.pos_config.pricelist_id.id,
  1539. 'amount_paid': 220.0,
  1540. 'amount_total': 220.0,
  1541. 'amount_tax': 20.0,
  1542. 'amount_return': 0.0,
  1543. 'to_invoice': False,
  1544. })
  1545. #make payment
  1546. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1547. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1548. 'amount': order.amount_total,
  1549. 'payment_method_id': self.cash_payment_method.id
  1550. })
  1551. order_payment.with_context(**payment_context).check()
  1552. session_id = self.pos_config.current_session_id
  1553. # closing the session with basic pos access
  1554. pos_user = self.env['res.users'].create({
  1555. 'name': "PoS user",
  1556. 'login': "pos_user",
  1557. 'email': "pos_user@yourcompany.com",
  1558. 'groups_id': [(6, 0, [self.ref('base.group_user'), self.ref('point_of_sale.group_pos_user')])],
  1559. })
  1560. self.pos_config.current_session_id.with_user(pos_user).action_pos_session_closing_control()
  1561. #get journal entries created
  1562. aml = session_id.move_id.line_ids.filtered(lambda x: x.account_id == self.account1 and x.tax_ids == self.tax1)
  1563. self.assertEqual(aml.price_total, 220)
  1564. self.assertEqual(aml.price_subtotal, 200)
  1565. def test_multi_exp_account_real_time(self):
  1566. #Create a real time valuation product category
  1567. self.real_time_categ = self.env['product.category'].create({
  1568. 'name': 'test category',
  1569. 'parent_id': False,
  1570. 'property_cost_method': 'fifo',
  1571. 'property_valuation': 'real_time',
  1572. })
  1573. #Create 2 accounts to be used for each product
  1574. self.account1 = self.env['account.account'].create({
  1575. 'name': 'Account 1',
  1576. 'code': 'AC1',
  1577. 'reconcile': True,
  1578. 'account_type': 'expense',
  1579. })
  1580. self.account2 = self.env['account.account'].create({
  1581. 'name': 'Account 1',
  1582. 'code': 'AC2',
  1583. 'reconcile': True,
  1584. 'account_type': 'expense',
  1585. })
  1586. self.product_a = self.env['product.product'].create({
  1587. 'name': 'Product A',
  1588. 'type': 'product',
  1589. 'categ_id': self.real_time_categ.id,
  1590. 'property_account_expense_id': self.account1.id,
  1591. 'property_account_income_id': self.account1.id,
  1592. })
  1593. self.product_b = self.env['product.product'].create({
  1594. 'name': 'Product B',
  1595. 'type': 'product',
  1596. 'categ_id': self.real_time_categ.id,
  1597. 'property_account_expense_id': self.account2.id,
  1598. 'property_account_income_id': self.account2.id,
  1599. })
  1600. #Create an order with the 2 products
  1601. self.pos_config.open_ui()
  1602. order = self.PosOrder.create({
  1603. 'company_id': self.env.company.id,
  1604. 'session_id': self.pos_config.current_session_id.id,
  1605. 'partner_id': self.partner1.id,
  1606. 'lines': [(0, 0, {
  1607. 'name': "OL/0001",
  1608. 'product_id': self.product_a.id,
  1609. 'price_unit': 100,
  1610. 'discount': 0,
  1611. 'qty': 1,
  1612. 'tax_ids': [],
  1613. 'price_subtotal': 100,
  1614. 'price_subtotal_incl': 100,
  1615. }), (0, 0, {
  1616. 'name': "OL/0002",
  1617. 'product_id': self.product_b.id,
  1618. 'price_unit': 100,
  1619. 'discount': 0,
  1620. 'qty': 1,
  1621. 'tax_ids': [],
  1622. 'price_subtotal': 100,
  1623. 'price_subtotal_incl': 100,
  1624. })],
  1625. 'pricelist_id': self.pos_config.pricelist_id.id,
  1626. 'amount_paid': 200.0,
  1627. 'amount_total': 200.0,
  1628. 'amount_tax': 0.0,
  1629. 'amount_return': 0.0,
  1630. 'to_invoice': False,
  1631. 'to_ship': True,
  1632. })
  1633. #make payment
  1634. payment_context = {"active_ids": order.ids, "active_id": order.id}
  1635. order_payment = self.PosMakePayment.with_context(**payment_context).create({
  1636. 'amount': order.amount_total,
  1637. 'payment_method_id': self.cash_payment_method.id
  1638. })
  1639. order_payment.with_context(**payment_context).check()
  1640. self.pos_config.current_session_id.action_pos_session_closing_control()
  1641. order.picking_ids._action_done()
  1642. moves = self.env['account.move'].search([('ref', '=', f'pos_order_{order.id}')])
  1643. self.assertEqual(len(moves), 2)