test_stockvaluation.py 109 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import time
  4. from datetime import datetime
  5. from freezegun import freeze_time
  6. from unittest.mock import patch
  7. import odoo
  8. from odoo import fields
  9. from odoo.tests import Form
  10. from odoo.tests.common import TransactionCase, tagged
  11. from odoo.addons.account.tests.common import AccountTestInvoicingCommon
  12. from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
  13. class TestStockValuation(TransactionCase):
  14. @classmethod
  15. def setUpClass(cls):
  16. super().setUpClass()
  17. cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
  18. cls.stock_location = cls.env.ref('stock.stock_location_stock')
  19. cls.partner_id = cls.env['res.partner'].create({
  20. 'name': 'Wood Corner Partner',
  21. 'company_id': cls.env.user.company_id.id,
  22. })
  23. cls.product1 = cls.env['product.product'].create({
  24. 'name': 'Large Desk',
  25. 'standard_price': 1299.0,
  26. 'list_price': 1799.0,
  27. # Ignore tax calculations for these tests.
  28. 'supplier_taxes_id': False,
  29. 'type': 'product',
  30. })
  31. Account = cls.env['account.account']
  32. cls.stock_input_account = Account.create({
  33. 'name': 'Stock Input',
  34. 'code': 'StockIn',
  35. 'account_type': 'asset_current',
  36. 'reconcile': True,
  37. })
  38. cls.stock_output_account = Account.create({
  39. 'name': 'Stock Output',
  40. 'code': 'StockOut',
  41. 'account_type': 'asset_current',
  42. 'reconcile': True,
  43. })
  44. cls.stock_valuation_account = Account.create({
  45. 'name': 'Stock Valuation',
  46. 'code': 'StockValuation',
  47. 'account_type': 'asset_current',
  48. })
  49. cls.stock_journal = cls.env['account.journal'].create({
  50. 'name': 'Stock Journal',
  51. 'code': 'STJTEST',
  52. 'type': 'general',
  53. })
  54. cls.product1.categ_id.write({
  55. 'property_valuation': 'real_time',
  56. 'property_stock_account_input_categ_id': cls.stock_input_account.id,
  57. 'property_stock_account_output_categ_id': cls.stock_output_account.id,
  58. 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
  59. 'property_stock_journal': cls.stock_journal.id,
  60. })
  61. cls.env.ref('base.EUR').active = True
  62. def test_different_uom(self):
  63. """ Set a quantity to replenish via the "Buy" route
  64. where product_uom is different from purchase uom
  65. """
  66. self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', False)
  67. # Create and set a new weight unit.
  68. kgm = self.env.ref('uom.product_uom_kgm')
  69. ap = self.env['uom.uom'].create({
  70. 'category_id': kgm.category_id.id,
  71. 'name': 'Algerian Pounds',
  72. 'uom_type': 'bigger',
  73. 'ratio': 2.47541,
  74. 'rounding': 0.001,
  75. })
  76. kgm_price = 100
  77. ap_price = kgm_price / ap.factor
  78. self.product1.uom_id = ap
  79. self.product1.uom_po_id = kgm
  80. # Set vendor
  81. vendor = self.env['res.partner'].create(dict(name='The Replenisher'))
  82. supplierinfo = self.env['product.supplierinfo'].create({
  83. 'partner_id': vendor.id,
  84. 'price': kgm_price,
  85. })
  86. self.product1.seller_ids = [(4, supplierinfo.id, 0)]
  87. # Automated stock valuation
  88. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  89. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  90. # Create a manual replenishment
  91. replenishment_uom_qty = 200
  92. replenish_wizard = self.env['product.replenish'].create({
  93. 'product_id': self.product1.id,
  94. 'product_tmpl_id': self.product1.product_tmpl_id.id,
  95. 'product_uom_id': ap.id,
  96. 'quantity': replenishment_uom_qty,
  97. 'warehouse_id': self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1).id,
  98. })
  99. replenish_wizard.launch_replenishment()
  100. last_po_id = self.env['purchase.order'].search([
  101. ('origin', 'ilike', '%Manual Replenishment%'),
  102. ('partner_id', '=', vendor.id)
  103. ])[-1]
  104. order_line = last_po_id.order_line.search([('product_id', '=', self.product1.id)])
  105. self.assertEqual(order_line.product_qty,
  106. ap._compute_quantity(replenishment_uom_qty, kgm, rounding_method='HALF-UP'),
  107. 'Quantities does not match')
  108. # Recieve products
  109. last_po_id.button_confirm()
  110. picking = last_po_id.picking_ids[0]
  111. move = picking.move_ids[0]
  112. move.quantity_done = move.product_uom_qty
  113. picking.button_validate()
  114. self.assertEqual(move.stock_valuation_layer_ids.unit_cost,
  115. last_po_id.currency_id.round(ap_price),
  116. "Wrong Unit price")
  117. def test_change_unit_cost_average_1(self):
  118. """ Confirm a purchase order and create the associated receipt, change the unit cost of the
  119. purchase order before validating the receipt, the value of the received goods should be set
  120. according to the last unit cost.
  121. """
  122. self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
  123. po1 = self.env['purchase.order'].create({
  124. 'partner_id': self.partner_id.id,
  125. 'order_line': [
  126. (0, 0, {
  127. 'name': self.product1.name,
  128. 'product_id': self.product1.id,
  129. 'product_qty': 10.0,
  130. 'product_uom': self.product1.uom_po_id.id,
  131. 'price_unit': 100.0,
  132. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  133. }),
  134. ],
  135. })
  136. po1.button_confirm()
  137. picking1 = po1.picking_ids[0]
  138. move1 = picking1.move_ids[0]
  139. # the unit price of the purchase order line is copied to the in move
  140. self.assertEqual(move1.price_unit, 100)
  141. # update the unit price on the purchase order line
  142. po1.order_line.price_unit = 200
  143. # validate the receipt
  144. res_dict = picking1.button_validate()
  145. wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
  146. wizard.process()
  147. # the unit price of the valuationlayer used the latest value
  148. self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 200)
  149. self.assertEqual(self.product1.value_svl, 2000)
  150. def test_standard_price_change_1(self):
  151. """ Confirm a purchase order and create the associated receipt, change the unit cost of the
  152. purchase order and the standard price of the product before validating the receipt, the
  153. value of the received goods should be set according to the last standard price.
  154. """
  155. self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
  156. # set a standard price
  157. self.product1.product_tmpl_id.standard_price = 10
  158. po1 = self.env['purchase.order'].create({
  159. 'partner_id': self.partner_id.id,
  160. 'order_line': [
  161. (0, 0, {
  162. 'name': self.product1.name,
  163. 'product_id': self.product1.id,
  164. 'product_qty': 10.0,
  165. 'product_uom': self.product1.uom_po_id.id,
  166. 'price_unit': 11.0,
  167. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  168. }),
  169. ],
  170. })
  171. po1.button_confirm()
  172. picking1 = po1.picking_ids[0]
  173. move1 = picking1.move_ids[0]
  174. # the move's unit price reflects the purchase order line's cost even if it's useless when
  175. # the product's cost method is standard
  176. self.assertEqual(move1.price_unit, 11)
  177. # set a new standard price
  178. self.product1.product_tmpl_id.standard_price = 12
  179. # the unit price on the stock move is not directly updated
  180. self.assertEqual(move1.price_unit, 11)
  181. # validate the receipt
  182. res_dict = picking1.button_validate()
  183. wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
  184. wizard.process()
  185. # the unit price of the valuation layer used the latest value
  186. self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 12)
  187. self.assertEqual(self.product1.value_svl, 120)
  188. def test_extra_move_fifo_1(self):
  189. """ Check that the extra move when over processing a receipt is correctly merged back in
  190. the original move.
  191. """
  192. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  193. po1 = self.env['purchase.order'].create({
  194. 'partner_id': self.partner_id.id,
  195. 'order_line': [
  196. (0, 0, {
  197. 'name': self.product1.name,
  198. 'product_id': self.product1.id,
  199. 'product_qty': 10.0,
  200. 'product_uom': self.product1.uom_po_id.id,
  201. 'price_unit': 100.0,
  202. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  203. }),
  204. ],
  205. })
  206. po1.button_confirm()
  207. picking1 = po1.picking_ids[0]
  208. move1 = picking1.move_ids[0]
  209. move1.quantity_done = 15
  210. picking1.button_validate()
  211. # there should be only one move
  212. self.assertEqual(len(picking1.move_ids), 1)
  213. self.assertEqual(move1.price_unit, 100)
  214. self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 100)
  215. self.assertEqual(move1.product_qty, 15)
  216. self.assertEqual(self.product1.value_svl, 1500)
  217. def test_backorder_fifo_1(self):
  218. """ Check that the backordered move when under processing a receipt correctly keep the
  219. price unit of the original move.
  220. """
  221. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  222. po1 = self.env['purchase.order'].create({
  223. 'partner_id': self.partner_id.id,
  224. 'order_line': [
  225. (0, 0, {
  226. 'name': self.product1.name,
  227. 'product_id': self.product1.id,
  228. 'product_qty': 10.0,
  229. 'product_uom': self.product1.uom_po_id.id,
  230. 'price_unit': 100.0,
  231. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  232. }),
  233. ],
  234. })
  235. po1.button_confirm()
  236. picking1 = po1.picking_ids[0]
  237. move1 = picking1.move_ids[0]
  238. move1.quantity_done = 5
  239. res_dict = picking1.button_validate()
  240. self.assertEqual(res_dict['res_model'], 'stock.backorder.confirmation')
  241. wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')).with_context(res_dict['context'])
  242. wizard.process()
  243. self.assertEqual(len(picking1.move_ids), 1)
  244. self.assertEqual(move1.price_unit, 100)
  245. self.assertEqual(move1.product_qty, 5)
  246. picking2 = po1.picking_ids.filtered(lambda p: p.backorder_id)
  247. move2 = picking2.move_ids[0]
  248. self.assertEqual(len(picking2.move_ids), 1)
  249. self.assertEqual(move2.price_unit, 100)
  250. self.assertEqual(move2.product_qty, 5)
  251. @tagged('post_install', '-at_install')
  252. class TestStockValuationWithCOA(AccountTestInvoicingCommon):
  253. @classmethod
  254. def setUpClass(cls, chart_template_ref=None):
  255. super().setUpClass(chart_template_ref=chart_template_ref)
  256. cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
  257. cls.stock_location = cls.env.ref('stock.stock_location_stock')
  258. cls.partner_id = cls.env['res.partner'].create({'name': 'Wood Corner Partner'})
  259. cls.product1 = cls.env['product.product'].create({'name': 'Large Desk'})
  260. cls.cat = cls.env['product.category'].create({
  261. 'name': 'cat',
  262. })
  263. cls.product1 = cls.env['product.product'].create({
  264. 'name': 'product1',
  265. 'type': 'product',
  266. 'categ_id': cls.cat.id,
  267. })
  268. cls.product1_copy = cls.env['product.product'].create({
  269. 'name': 'product1',
  270. 'type': 'product',
  271. 'categ_id': cls.cat.id,
  272. })
  273. Account = cls.env['account.account']
  274. cls.usd_currency = cls.env.ref('base.USD')
  275. cls.eur_currency = cls.env.ref('base.EUR')
  276. cls.usd_currency.active = True
  277. cls.eur_currency.active = True
  278. cls.stock_input_account = Account.create({
  279. 'name': 'Stock Input',
  280. 'code': 'StockIn',
  281. 'account_type': 'asset_current',
  282. 'reconcile': True,
  283. })
  284. cls.stock_output_account = Account.create({
  285. 'name': 'Stock Output',
  286. 'code': 'StockOut',
  287. 'account_type': 'asset_current',
  288. 'reconcile': True,
  289. })
  290. cls.stock_valuation_account = Account.create({
  291. 'name': 'Stock Valuation',
  292. 'code': 'StockValuation',
  293. 'account_type': 'asset_current',
  294. })
  295. cls.price_diff_account = Account.create({
  296. 'name': 'price diff account',
  297. 'code': 'priceDiffAccount',
  298. 'account_type': 'asset_current',
  299. })
  300. cls.stock_journal = cls.env['account.journal'].create({
  301. 'name': 'Stock Journal',
  302. 'code': 'STJTEST',
  303. 'type': 'general',
  304. })
  305. cls.product1.categ_id.write({
  306. 'property_stock_account_input_categ_id': cls.stock_input_account.id,
  307. 'property_stock_account_output_categ_id': cls.stock_output_account.id,
  308. 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
  309. 'property_stock_journal': cls.stock_journal.id,
  310. 'property_account_creditor_price_difference_categ': cls.product1.product_tmpl_id.get_product_accounts()['expense'],
  311. 'property_valuation': 'real_time',
  312. })
  313. old_action_post = odoo.addons.account.models.account_move.AccountMove.action_post
  314. old_create = odoo.models.BaseModel.create
  315. def new_action_post(self):
  316. """ Force the creation of tracking values. """
  317. res = old_action_post(self)
  318. if self:
  319. cls.env.flush_all()
  320. cls.cr.flush()
  321. return res
  322. def new_create(self, vals_list):
  323. cls.cr._now = datetime.now()
  324. return old_create(self, vals_list)
  325. post_patch = patch('odoo.addons.account.models.account_move.AccountMove.action_post', new_action_post)
  326. create_patch = patch('odoo.models.BaseModel.create', new_create)
  327. cls.startClassPatcher(post_patch)
  328. cls.startClassPatcher(create_patch)
  329. def _bill(self, po, qty=None, price=None):
  330. action = po.action_create_invoice()
  331. bill = self.env["account.move"].browse(action["res_id"])
  332. bill.invoice_date = fields.Date.today()
  333. if qty is not None:
  334. bill.invoice_line_ids.quantity = qty
  335. if price is not None:
  336. bill.invoice_line_ids.price_unit = price
  337. bill.action_post()
  338. return bill
  339. def _refund(self, inv, qty=None):
  340. ctx = {'active_ids': inv.ids, 'active_id': inv.id, 'active_model': 'account.move'}
  341. method = 'cancel' if qty is None else 'refund'
  342. credit_note_wizard = self.env['account.move.reversal'].with_context(ctx).create({
  343. 'refund_method': method,
  344. 'journal_id': inv.journal_id.id,
  345. })
  346. rinv = self.env['account.move'].browse(credit_note_wizard.reverse_moves()['res_id'])
  347. if method == 'refund':
  348. rinv.invoice_line_ids.quantity = qty
  349. rinv.action_post()
  350. return rinv
  351. def _return(self, picking, qty=None):
  352. wizard_form = Form(self.env['stock.return.picking'].with_context(active_ids=picking.ids, active_id=picking.id, active_model='stock.picking'))
  353. wizard = wizard_form.save()
  354. qty = qty or wizard.product_return_moves.quantity
  355. wizard.product_return_moves.quantity = qty
  356. action = wizard.create_returns()
  357. return_picking = self.env["stock.picking"].browse(action["res_id"])
  358. return_picking.move_ids.move_line_ids.qty_done = qty
  359. return_picking.button_validate()
  360. return return_picking
  361. def test_change_currency_rate_average_1(self):
  362. """ Confirm a purchase order in another currency and create the associated receipt, change
  363. the currency rate, validate the receipt and then check that the value of the received goods
  364. is set according to the last currency rate.
  365. """
  366. self.env['res.currency.rate'].search([]).unlink()
  367. usd_currency = self.env.ref('base.USD')
  368. self.env.company.currency_id = usd_currency.id
  369. eur_currency = self.env.ref('base.EUR')
  370. self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
  371. # default currency is USD, create a purchase order in EUR
  372. po1 = self.env['purchase.order'].create({
  373. 'partner_id': self.partner_id.id,
  374. 'currency_id': eur_currency.id,
  375. 'order_line': [
  376. (0, 0, {
  377. 'name': self.product1.name,
  378. 'product_id': self.product1.id,
  379. 'product_qty': 10.0,
  380. 'product_uom': self.product1.uom_po_id.id,
  381. 'price_unit': 100.0,
  382. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  383. }),
  384. ],
  385. })
  386. po1.button_confirm()
  387. picking1 = po1.picking_ids[0]
  388. move1 = picking1.move_ids[0]
  389. # convert the price unit in the company currency
  390. price_unit_usd = po1.currency_id._convert(
  391. po1.order_line.price_unit, po1.company_id.currency_id,
  392. self.env.company, fields.Date.today(), round=False)
  393. # the unit price of the move is the unit price of the purchase order line converted in
  394. # the company's currency
  395. self.assertAlmostEqual(move1.price_unit, price_unit_usd, places=2)
  396. # change the rate of the currency
  397. self.env['res.currency.rate'].create({
  398. 'name': time.strftime('%Y-%m-%d'),
  399. 'rate': 2.0,
  400. 'currency_id': eur_currency.id,
  401. 'company_id': po1.company_id.id,
  402. })
  403. eur_currency._compute_current_rate()
  404. price_unit_usd_new_rate = po1.currency_id._convert(
  405. po1.order_line.price_unit, po1.company_id.currency_id,
  406. self.env.company, fields.Date.today(), round=False)
  407. # the new price_unit is lower than th initial because of the rate's change
  408. self.assertLess(price_unit_usd_new_rate, price_unit_usd)
  409. # the unit price on the stock move is not directly updated
  410. self.assertAlmostEqual(move1.price_unit, price_unit_usd, places=2)
  411. # validate the receipt
  412. res_dict = picking1.button_validate()
  413. wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
  414. wizard.process()
  415. # the unit price of the valuation layer used the latest value
  416. self.assertAlmostEqual(move1.stock_valuation_layer_ids.unit_cost, price_unit_usd_new_rate)
  417. self.assertAlmostEqual(self.product1.value_svl, price_unit_usd_new_rate * 10, delta=0.1)
  418. def test_fifo_anglosaxon_return(self):
  419. self.env.company.anglo_saxon_accounting = True
  420. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  421. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  422. # Receive 10@10 ; create the vendor bill
  423. po1 = self.env['purchase.order'].create({
  424. 'partner_id': self.partner_id.id,
  425. 'order_line': [
  426. (0, 0, {
  427. 'name': self.product1.name,
  428. 'product_id': self.product1.id,
  429. 'product_qty': 10.0,
  430. 'product_uom': self.product1.uom_po_id.id,
  431. 'price_unit': 10.0,
  432. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  433. }),
  434. ],
  435. })
  436. po1.button_confirm()
  437. receipt_po1 = po1.picking_ids[0]
  438. receipt_po1.move_ids.quantity_done = 10
  439. receipt_po1.button_validate()
  440. move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  441. move_form.invoice_date = move_form.date
  442. move_form.partner_id = self.partner_id
  443. move_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po1.id)
  444. invoice_po1 = move_form.save()
  445. invoice_po1.action_post()
  446. # Receive 10@20 ; create the vendor bill
  447. po2 = self.env['purchase.order'].create({
  448. 'partner_id': self.partner_id.id,
  449. 'order_line': [
  450. (0, 0, {
  451. 'name': self.product1.name,
  452. 'product_id': self.product1.id,
  453. 'product_qty': 10.0,
  454. 'product_uom': self.product1.uom_po_id.id,
  455. 'price_unit': 20.0,
  456. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  457. }),
  458. ],
  459. })
  460. po2.button_confirm()
  461. receipt_po2 = po2.picking_ids[0]
  462. receipt_po2.move_ids.quantity_done = 10
  463. receipt_po2.button_validate()
  464. move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  465. move_form.invoice_date = move_form.date
  466. move_form.partner_id = self.partner_id
  467. move_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po2.id)
  468. invoice_po2 = move_form.save()
  469. invoice_po2.action_post()
  470. # valuation of product1 should be 300
  471. self.assertEqual(self.product1.value_svl, 300)
  472. # return the second po
  473. stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(
  474. active_ids=receipt_po2.ids, active_id=receipt_po2.ids[0], active_model='stock.picking'))
  475. stock_return_picking = stock_return_picking_form.save()
  476. stock_return_picking.product_return_moves.quantity = 10
  477. stock_return_picking_action = stock_return_picking.create_returns()
  478. return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
  479. return_pick.move_ids[0].move_line_ids[0].qty_done = 10
  480. return_pick.button_validate()
  481. # valuation of product1 should be 200 as the first items will be sent out
  482. self.assertEqual(self.product1.value_svl, 200)
  483. # create a credit note for po2
  484. move_form = Form(self.env['account.move'].with_context(default_move_type='in_refund'))
  485. move_form.invoice_date = move_form.date
  486. move_form.partner_id = self.partner_id
  487. # Not supposed to see/change the purchase order of a refund invoice by default
  488. # <field name="purchase_id" invisible="1"/>
  489. # <label for="purchase_vendor_bill_id" string="Auto-Complete" class="oe_edit_only"
  490. # attrs="{'invisible': ['|', ('state','!=','draft'), ('move_type', '!=', 'in_invoice')]}" />
  491. # <field name="purchase_vendor_bill_id" nolabel="1"
  492. # attrs="{'invisible': ['|', ('state','!=','draft'), ('move_type', '!=', 'in_invoice')]}"
  493. move_form._view['modifiers']['purchase_id']['invisible'] = False
  494. move_form.purchase_id = po2
  495. with move_form.invoice_line_ids.edit(0) as line_form:
  496. line_form.quantity = 10
  497. creditnote_po2 = move_form.save()
  498. creditnote_po2.action_post()
  499. # check the anglo saxon entries
  500. price_diff_entry = self.env['account.move.line'].search([
  501. ('account_id', '=', self.stock_valuation_account.id),
  502. ('move_id.stock_move_id', '=', return_pick.move_ids[0].id)])
  503. self.assertEqual(price_diff_entry.credit, 100)
  504. def test_anglosaxon_valuation(self):
  505. self.env.company.anglo_saxon_accounting = True
  506. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  507. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  508. # Create PO
  509. po_form = Form(self.env['purchase.order'])
  510. po_form.partner_id = self.partner_id
  511. with po_form.order_line.new() as po_line:
  512. po_line.product_id = self.product1
  513. po_line.product_qty = 1
  514. po_line.price_unit = 10.0
  515. order = po_form.save()
  516. order.button_confirm()
  517. # Receive the goods
  518. receipt = order.picking_ids[0]
  519. receipt.move_ids.quantity_done = 1
  520. receipt.button_validate()
  521. stock_valuation_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  522. receipt_aml = stock_valuation_aml[0]
  523. self.assertEqual(len(stock_valuation_aml), 1, "For now, only one line for the stock valuation account")
  524. self.assertAlmostEqual(receipt_aml.debit, 10, msg="Should be equal to the PO line unit price (10)")
  525. # Create an invoice with a different price
  526. move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  527. move_form.invoice_date = move_form.date
  528. move_form.partner_id = order.partner_id
  529. move_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-order.id)
  530. with move_form.invoice_line_ids.edit(0) as line_form:
  531. line_form.price_unit = 15.0
  532. invoice = move_form.save()
  533. invoice.action_post()
  534. # Check what was posted in the stock valuation account
  535. stock_valuation_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  536. price_diff_aml = stock_valuation_aml - receipt_aml
  537. self.assertEqual(len(stock_valuation_aml), 2, "A second line should have been generated for the price difference.")
  538. self.assertAlmostEqual(price_diff_aml.debit, 5, msg="Price difference should be equal to 5 (15-10)")
  539. self.assertAlmostEqual(
  540. sum(stock_valuation_aml.mapped('debit')), 15,
  541. msg="Total debit value on stock valuation account should be equal to the invoiced price of the product.")
  542. # Check what was posted in stock input account
  543. input_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  544. self.assertEqual(len(input_aml), 3, "Only three lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
  545. invoice_amls = input_aml.filtered(lambda l: l.move_id == invoice)
  546. picking_aml = input_aml - invoice_amls
  547. self.assertEqual(sum(invoice_amls.mapped('debit')), 15, "Total debit value on stock input account should be equal to the invoice price of the product.")
  548. self.assertEqual(sum(invoice_amls.mapped('credit')), 0, "Invoice account move lines should not contains information on stock input at this point.")
  549. self.assertEqual(sum(picking_aml.mapped('credit')), 15, "Total credit value on stock input account should be equal to the invoice price of the product.")
  550. def test_valuation_from_increasing_tax(self):
  551. """ Check that a tax without account will increment the stock value.
  552. """
  553. tax_with_no_account = self.env['account.tax'].create({
  554. 'name': "Tax with no account",
  555. 'amount_type': 'fixed',
  556. 'amount': 5,
  557. 'sequence': 8,
  558. })
  559. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  560. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  561. # Receive 10@10 ; create the vendor bill
  562. po1 = self.env['purchase.order'].create({
  563. 'partner_id': self.partner_id.id,
  564. 'order_line': [
  565. (0, 0, {
  566. 'name': self.product1.name,
  567. 'product_id': self.product1.id,
  568. 'taxes_id': [(4, tax_with_no_account.id)],
  569. 'product_qty': 10.0,
  570. 'product_uom': self.product1.uom_po_id.id,
  571. 'price_unit': 10.0,
  572. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  573. }),
  574. ],
  575. })
  576. po1.button_confirm()
  577. receipt_po1 = po1.picking_ids[0]
  578. receipt_po1.move_ids.quantity_done = 10
  579. receipt_po1.button_validate()
  580. # valuation of product1 should be 15 as the tax with no account set
  581. # has gone to the stock account, and must be reflected in inventory valuation
  582. self.assertEqual(self.product1.value_svl, 150)
  583. def test_standard_valuation_multicurrency(self):
  584. company = self.env.user.company_id
  585. company.anglo_saxon_accounting = True
  586. company.currency_id = self.usd_currency
  587. date_po = '2019-01-01'
  588. self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
  589. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  590. self.product1.standard_price = 10
  591. # SetUp currency and rates 1$ = 2 Euros
  592. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  593. self.env['res.currency.rate'].search([]).unlink()
  594. self.env['res.currency.rate'].create({
  595. 'name': date_po,
  596. 'rate': 1.0,
  597. 'currency_id': self.usd_currency.id,
  598. 'company_id': company.id,
  599. })
  600. self.env['res.currency.rate'].create({
  601. 'name': date_po,
  602. 'rate': 2,
  603. 'currency_id': self.eur_currency.id,
  604. 'company_id': company.id,
  605. })
  606. # Create PO
  607. po = self.env['purchase.order'].create({
  608. 'currency_id': self.eur_currency.id,
  609. 'partner_id': self.partner_id.id,
  610. 'order_line': [
  611. (0, 0, {
  612. 'name': self.product1.name,
  613. 'product_id': self.product1.id,
  614. 'product_qty': 1.0,
  615. 'product_uom': self.product1.uom_po_id.id,
  616. 'price_unit': 100.0, # 50$
  617. 'date_planned': date_po,
  618. }),
  619. ],
  620. })
  621. po.button_confirm()
  622. # Receive the goods
  623. receipt = po.picking_ids[0]
  624. receipt.move_line_ids.qty_done = 1
  625. receipt.button_validate()
  626. # Create a vendor bill
  627. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  628. 'move_type': 'in_invoice',
  629. 'invoice_date': date_po,
  630. 'date': date_po,
  631. 'currency_id': self.eur_currency.id,
  632. 'partner_id': self.partner_id.id,
  633. 'invoice_line_ids': [(0, 0, {
  634. 'name': 'Test',
  635. 'price_unit': 100.0,
  636. 'product_id': self.product1.id,
  637. 'purchase_line_id': po.order_line.id,
  638. 'quantity': 1.0,
  639. 'account_id': self.stock_input_account.id,
  640. })]
  641. })
  642. inv.action_post()
  643. # Check what was posted in stock input account
  644. input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  645. self.assertEqual(len(input_amls), 3, "Only three lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
  646. invoice_amls = input_amls.filtered(lambda l: l.move_id == inv)
  647. picking_aml = input_amls - invoice_amls
  648. payable_aml = invoice_amls.filtered(lambda l: l.amount_currency > 0)
  649. diff_aml = invoice_amls - payable_aml
  650. # check USD
  651. self.assertAlmostEqual(payable_aml.debit, 50, msg="Total debit value should be equal to the original PO price of the product.")
  652. self.assertAlmostEqual(picking_aml.credit, 10, msg="credit value for stock should be equal to the standard price of the product.")
  653. self.assertAlmostEqual(diff_aml.credit, 40, msg="credit value for price difference")
  654. # check EUR
  655. self.assertAlmostEqual(payable_aml.amount_currency, 100, msg="Total debit value should be equal to the original PO price of the product.")
  656. self.assertAlmostEqual(picking_aml.amount_currency, -20, msg="credit value for stock should be equal to the standard price of the product.")
  657. self.assertAlmostEqual(diff_aml.amount_currency, -80, msg="credit value for price difference")
  658. def test_valuation_multicurecny_with_tax(self):
  659. """ Check that a tax without account will increment the stock value.
  660. """
  661. company = self.env.user.company_id
  662. company.anglo_saxon_accounting = True
  663. company.currency_id = self.usd_currency
  664. date_po = '2019-01-01'
  665. self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
  666. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  667. # SetUp currency and rates 1$ = 2Euros
  668. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  669. self.env['res.currency.rate'].search([]).unlink()
  670. self.env['res.currency.rate'].create({
  671. 'name': date_po,
  672. 'rate': 1.0,
  673. 'currency_id': self.usd_currency.id,
  674. 'company_id': company.id,
  675. })
  676. self.env['res.currency.rate'].create({
  677. 'name': date_po,
  678. 'rate': 2,
  679. 'currency_id': self.eur_currency.id,
  680. 'company_id': company.id,
  681. })
  682. tax_with_no_account = self.env['account.tax'].create({
  683. 'name': "Tax with no account",
  684. 'amount_type': 'fixed',
  685. 'amount': 5,
  686. 'sequence': 8,
  687. 'price_include': True,
  688. })
  689. # Create PO
  690. po = self.env['purchase.order'].create({
  691. 'currency_id': self.eur_currency.id,
  692. 'partner_id': self.partner_id.id,
  693. 'order_line': [
  694. (0, 0, {
  695. 'name': self.product1.name,
  696. 'product_id': self.product1.id,
  697. 'product_qty': 1.0,
  698. 'product_uom': self.product1.uom_po_id.id,
  699. 'price_unit': 100.0, # 50$
  700. 'taxes_id': [(4, tax_with_no_account.id)],
  701. 'date_planned': date_po,
  702. }),
  703. ],
  704. })
  705. po.button_confirm()
  706. # Receive the goods
  707. receipt = po.picking_ids[0]
  708. receipt.move_line_ids.qty_done = 1
  709. receipt.button_validate()
  710. # Create a vendor bill
  711. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  712. 'move_type': 'in_invoice',
  713. 'invoice_date': date_po,
  714. 'date': date_po,
  715. 'currency_id': self.eur_currency.id,
  716. 'partner_id': self.partner_id.id,
  717. 'invoice_line_ids': [(0, 0, {
  718. 'name': 'Test',
  719. 'price_unit': 100.0,
  720. 'product_id': self.product1.id,
  721. 'purchase_line_id': po.order_line.id,
  722. 'quantity': 1.0,
  723. 'account_id': self.stock_input_account.id,
  724. })]
  725. })
  726. inv.action_post()
  727. invoice_aml = inv.invoice_line_ids
  728. picking_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  729. # check EUR
  730. self.assertAlmostEqual(invoice_aml.amount_currency, 100, msg="Total debit value should be equal to the original PO price of the product.")
  731. self.assertAlmostEqual(picking_aml.amount_currency, 95, msg="credit value for stock should be equal to the untaxed price of the product.")
  732. def test_average_realtime_anglo_saxon_valuation_multicurrency_same_date(self):
  733. """
  734. The PO and invoice are in the same foreign currency.
  735. The PO is invoiced on the same date as its creation.
  736. This shouldn't create a price difference entry.
  737. """
  738. company = self.env.user.company_id
  739. company.anglo_saxon_accounting = True
  740. company.currency_id = self.usd_currency
  741. date_po = '2019-01-01'
  742. # SetUp product
  743. self.product1.product_tmpl_id.cost_method = 'average'
  744. self.product1.product_tmpl_id.valuation = 'real_time'
  745. self.product1.product_tmpl_id.purchase_method = 'purchase'
  746. # SetUp currency and rates
  747. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  748. self.env['res.currency.rate'].search([]).unlink()
  749. self.env['res.currency.rate'].create({
  750. 'name': date_po,
  751. 'rate': 1.0,
  752. 'currency_id': self.usd_currency.id,
  753. 'company_id': company.id,
  754. })
  755. self.env['res.currency.rate'].create({
  756. 'name': date_po,
  757. 'rate': 1.5,
  758. 'currency_id': self.eur_currency.id,
  759. 'company_id': company.id,
  760. })
  761. # Proceed
  762. po = self.env['purchase.order'].create({
  763. 'currency_id': self.eur_currency.id,
  764. 'partner_id': self.partner_id.id,
  765. 'order_line': [
  766. (0, 0, {
  767. 'name': self.product1.name,
  768. 'product_id': self.product1.id,
  769. 'product_qty': 1.0,
  770. 'product_uom': self.product1.uom_po_id.id,
  771. 'price_unit': 100.0,
  772. 'date_planned': date_po,
  773. }),
  774. ],
  775. })
  776. po.button_confirm()
  777. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  778. 'move_type': 'in_invoice',
  779. 'invoice_date': date_po,
  780. 'date': date_po,
  781. 'currency_id': self.eur_currency.id,
  782. 'partner_id': self.partner_id.id,
  783. 'invoice_line_ids': [(0, 0, {
  784. 'name': 'Test',
  785. 'price_unit': 100.0,
  786. 'product_id': self.product1.id,
  787. 'purchase_line_id': po.order_line.id,
  788. 'quantity': 1.0,
  789. 'account_id': self.stock_input_account.id,
  790. 'tax_ids': [],
  791. })]
  792. })
  793. inv.action_post()
  794. move_lines = inv.line_ids
  795. self.assertEqual(len(move_lines), 4)
  796. payable_line = move_lines.filtered(lambda l: l.account_id.account_type == 'liability_payable')
  797. self.assertEqual(payable_line.amount_currency, -100.0)
  798. self.assertAlmostEqual(payable_line.balance, -66.67)
  799. stock_line = move_lines.filtered(lambda l: l.account_id == self.stock_input_account and l.balance > 0)
  800. self.assertEqual(stock_line.amount_currency, 100.0)
  801. self.assertAlmostEqual(stock_line.balance, 66.67)
  802. def test_realtime_anglo_saxon_valuation_multicurrency_different_dates(self):
  803. """
  804. The PO and invoice are in the same foreign currency.
  805. The PO is invoiced at a later date than its creation.
  806. This should create a price difference entry for standard cost method
  807. Not for average cost method though, since the PO and invoice have the same currency
  808. """
  809. company = self.env.user.company_id
  810. company.anglo_saxon_accounting = True
  811. company.currency_id = self.usd_currency
  812. self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
  813. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  814. date_po = '2019-01-01'
  815. date_invoice = '2019-01-16'
  816. # SetUp product Average
  817. self.product1.product_tmpl_id.purchase_method = 'purchase'
  818. # SetUp product Standard
  819. # should have bought at 60 USD
  820. # actually invoiced at 70 EUR > 35 USD
  821. product_categ_standard = self.cat.copy({
  822. 'property_cost_method': 'standard',
  823. 'property_stock_account_input_categ_id': self.stock_input_account.id,
  824. 'property_stock_account_output_categ_id': self.stock_output_account.id,
  825. 'property_stock_valuation_account_id': self.stock_valuation_account.id,
  826. 'property_stock_journal': self.stock_journal.id,
  827. })
  828. product_standard = self.product1_copy
  829. product_standard.write({
  830. 'categ_id': product_categ_standard.id,
  831. 'name': 'Standard Val',
  832. 'standard_price': 60,
  833. })
  834. product_standard.product_tmpl_id.purchase_method = 'purchase'
  835. # SetUp currency and rates
  836. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  837. self.env['res.currency.rate'].search([]).unlink()
  838. self.env['res.currency.rate'].create({
  839. 'name': date_po,
  840. 'rate': 1.0,
  841. 'currency_id': self.usd_currency.id,
  842. 'company_id': company.id,
  843. })
  844. self.env['res.currency.rate'].create({
  845. 'name': date_po,
  846. 'rate': 1.5,
  847. 'currency_id': self.eur_currency.id,
  848. 'company_id': company.id,
  849. })
  850. self.env['res.currency.rate'].create({
  851. 'name': date_invoice,
  852. 'rate': 2,
  853. 'currency_id': self.eur_currency.id,
  854. 'company_id': company.id,
  855. })
  856. # To allow testing validation of PO
  857. def _today(*args, **kwargs):
  858. return date_po
  859. patchers = [
  860. patch('odoo.fields.Date.context_today', _today),
  861. ]
  862. for patcher in patchers:
  863. self.startPatcher(patcher)
  864. # Proceed
  865. po = self.env['purchase.order'].create({
  866. 'currency_id': self.eur_currency.id,
  867. 'partner_id': self.partner_id.id,
  868. 'order_line': [
  869. (0, 0, {
  870. 'name': self.product1.name,
  871. 'product_id': self.product1.id,
  872. 'product_qty': 1.0,
  873. 'product_uom': self.product1.uom_po_id.id,
  874. 'price_unit': 100.0,
  875. 'date_planned': date_po,
  876. }),
  877. (0, 0, {
  878. 'name': product_standard.name,
  879. 'product_id': product_standard.id,
  880. 'product_qty': 1.0,
  881. 'product_uom': product_standard.uom_po_id.id,
  882. 'price_unit': 40.0,
  883. 'date_planned': date_po,
  884. }),
  885. ],
  886. })
  887. po.button_confirm()
  888. line_product_average = po.order_line.filtered(lambda l: l.product_id == self.product1)
  889. line_product_standard = po.order_line.filtered(lambda l: l.product_id == product_standard)
  890. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  891. 'move_type': 'in_invoice',
  892. 'invoice_date': date_invoice,
  893. 'date': date_invoice,
  894. 'currency_id': self.eur_currency.id,
  895. 'partner_id': self.partner_id.id,
  896. 'invoice_line_ids': [
  897. (0, 0, {
  898. 'name': self.product1.name,
  899. 'price_subtotal': 100.0,
  900. 'price_unit': 100.0,
  901. 'product_id': self.product1.id,
  902. 'purchase_line_id': line_product_average.id,
  903. 'quantity': 1.0,
  904. 'account_id': self.stock_input_account.id,
  905. 'tax_ids': [],
  906. }),
  907. (0, 0, {
  908. 'name': product_standard.name,
  909. 'price_subtotal': 70.0,
  910. 'price_unit': 70.0,
  911. 'product_id': product_standard.id,
  912. 'purchase_line_id': line_product_standard.id,
  913. 'quantity': 1.0,
  914. 'account_id': self.stock_input_account.id,
  915. 'tax_ids': [],
  916. })
  917. ]
  918. })
  919. inv.action_post()
  920. move_lines = inv.line_ids
  921. self.assertEqual(len(move_lines), 3)
  922. # Ensure no exchange difference move has been created
  923. self.assertTrue(all([not l.reconciled for l in move_lines]))
  924. # PAYABLE CHECK
  925. payable_line = move_lines.filtered(lambda l: l.account_id.account_type == 'liability_payable')
  926. self.assertEqual(payable_line.amount_currency, -170.0)
  927. self.assertAlmostEqual(payable_line.balance, -85.00)
  928. # PRODUCTS CHECKS
  929. # NO EXCHANGE DIFFERENCE (average)
  930. # We ordered for a value of 100 EUR
  931. # But by the time we are invoiced for it
  932. # the foreign currency appreciated from 1.5 to 2.0
  933. # We still have to pay 100 EUR, which now values at 50 USD
  934. product_lines = move_lines.filtered(lambda l: l.product_id == self.product1)
  935. # Stock-wise, we have been invoiced 100 EUR, and we ordered 100 EUR
  936. # there is no price difference
  937. # However, 100 EUR should be converted at the time of the invoice
  938. stock_lines = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
  939. self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 100.00)
  940. self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 50.00)
  941. # PRICE DIFFERENCE (STANDARD)
  942. # We ordered a product that should have cost 60 USD (120 EUR)
  943. # However, we effectively got invoiced 70 EUR (35 USD)
  944. product_lines = move_lines.filtered(lambda l: l.product_id == product_standard)
  945. stock_lines = product_lines.filtered(lambda l: l.account_id == self.stock_input_account)
  946. self.assertAlmostEqual(sum(stock_lines.mapped('amount_currency')), 70.00)
  947. self.assertAlmostEqual(sum(stock_lines.mapped('balance')), 35.00)
  948. # TODO It should be evaluated during the receiption now
  949. # price_diff_line = product_lines.filtered(lambda l: l.account_id == self.stock_valuation_account)
  950. # self.assertEqual(price_diff_line.amount_currency, -50.00)
  951. # self.assertAlmostEqual(price_diff_line.balance, -25.00)
  952. def test_average_realtime_with_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
  953. """
  954. The PO and invoice are in the same foreign currency.
  955. The delivery occurs in between PO validation and invoicing
  956. The invoice is created at an even different date
  957. This should create a price difference entry.
  958. """
  959. company = self.env.user.company_id
  960. company.anglo_saxon_accounting = True
  961. company.currency_id = self.usd_currency
  962. self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
  963. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  964. date_po = '2019-01-01'
  965. date_delivery = '2019-01-08'
  966. date_invoice = '2019-01-16'
  967. product_avg = self.product1_copy
  968. product_avg.write({
  969. 'purchase_method': 'purchase',
  970. 'name': 'AVG',
  971. 'standard_price': 60,
  972. })
  973. # SetUp currency and rates
  974. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  975. self.env['res.currency.rate'].search([]).unlink()
  976. self.env['res.currency.rate'].create({
  977. 'name': date_po,
  978. 'rate': 1.0,
  979. 'currency_id': self.usd_currency.id,
  980. 'company_id': company.id,
  981. })
  982. self.env['res.currency.rate'].create({
  983. 'name': date_po,
  984. 'rate': 1.5,
  985. 'currency_id': self.eur_currency.id,
  986. 'company_id': company.id,
  987. })
  988. self.env['res.currency.rate'].create({
  989. 'name': date_delivery,
  990. 'rate': 0.7,
  991. 'currency_id': self.eur_currency.id,
  992. 'company_id': company.id,
  993. })
  994. self.env['res.currency.rate'].create({
  995. 'name': date_invoice,
  996. 'rate': 2,
  997. 'currency_id': self.eur_currency.id,
  998. 'company_id': company.id,
  999. })
  1000. # To allow testing validation of PO and Delivery
  1001. today = date_po
  1002. def _today(*args, **kwargs):
  1003. return datetime.strptime(today, "%Y-%m-%d").date()
  1004. def _now(*args, **kwargs):
  1005. return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
  1006. patchers = [
  1007. patch('odoo.fields.Date.context_today', _today),
  1008. patch('odoo.fields.Datetime.now', _now),
  1009. ]
  1010. for patcher in patchers:
  1011. self.startPatcher(patcher)
  1012. # Proceed
  1013. po = self.env['purchase.order'].create({
  1014. 'currency_id': self.eur_currency.id,
  1015. 'partner_id': self.partner_id.id,
  1016. 'order_line': [
  1017. (0, 0, {
  1018. 'name': product_avg.name,
  1019. 'product_id': product_avg.id,
  1020. 'product_qty': 1.0,
  1021. 'product_uom': product_avg.uom_po_id.id,
  1022. 'price_unit': 30.0,
  1023. 'date_planned': date_po,
  1024. })
  1025. ],
  1026. })
  1027. po.button_confirm()
  1028. line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
  1029. today = date_delivery
  1030. picking = po.picking_ids
  1031. (picking.move_ids
  1032. .filtered(lambda l: l.purchase_line_id == line_product_avg)
  1033. .write({'quantity_done': 1.0}))
  1034. picking.button_validate()
  1035. # 5 Units received at rate 0.7 = 42.86
  1036. self.assertAlmostEqual(product_avg.standard_price, 42.86)
  1037. today = date_invoice
  1038. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  1039. 'move_type': 'in_invoice',
  1040. 'invoice_date': date_invoice,
  1041. 'date': date_invoice,
  1042. 'currency_id': self.eur_currency.id,
  1043. 'partner_id': self.partner_id.id,
  1044. 'invoice_line_ids': [
  1045. (0, 0, {
  1046. 'name': product_avg.name,
  1047. 'price_unit': 30.0,
  1048. 'product_id': product_avg.id,
  1049. 'purchase_line_id': line_product_avg.id,
  1050. 'quantity': 1.0,
  1051. 'account_id': self.stock_input_account.id,
  1052. 'tax_ids': [],
  1053. })
  1054. ]
  1055. })
  1056. inv.action_post()
  1057. self.assertRecordValues(inv.line_ids, [
  1058. # pylint: disable=C0326
  1059. {'balance': 15.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id},
  1060. {'balance': -15.0, 'amount_currency': -30.0, 'account_id': self.company_data['default_account_payable'].id},
  1061. ])
  1062. self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids, [
  1063. # pylint: disable=C0326
  1064. {'balance': -42.86, 'amount_currency': -30.0, 'account_id': self.stock_input_account.id},
  1065. {'balance': 27.86, 'amount_currency': 0.0, 'account_id': self.stock_input_account.id},
  1066. {'balance': 15.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id},
  1067. ])
  1068. input_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  1069. self.assertEqual(len(input_aml), 3)
  1070. def test_average_realtime_with_delivery_anglo_saxon_valuation_multicurrency_same_dates(self):
  1071. """ Same than test_average_realtime_with_delivery_anglo_saxon_valuation_multicurrency_different_dates
  1072. but the rates change happens
  1073. """
  1074. company = self.env.user.company_id
  1075. company.anglo_saxon_accounting = True
  1076. company.currency_id = self.usd_currency
  1077. self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
  1078. self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
  1079. date = fields.Date.to_string(fields.Date.today())
  1080. product_avg = self.product1_copy
  1081. product_avg.write({
  1082. 'purchase_method': 'purchase',
  1083. 'name': 'AVG',
  1084. 'standard_price': 60,
  1085. })
  1086. # SetUp currency and rates
  1087. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  1088. self.env['res.currency.rate'].search([]).unlink()
  1089. self.env['res.currency.rate'].create({
  1090. 'name': date,
  1091. 'rate': 1.0,
  1092. 'currency_id': self.usd_currency.id,
  1093. 'company_id': company.id,
  1094. })
  1095. eur_rate = self.env['res.currency.rate'].create({
  1096. 'name': date,
  1097. 'rate': 0.5,
  1098. 'currency_id': self.eur_currency.id,
  1099. 'company_id': company.id,
  1100. })
  1101. # Proceed
  1102. po = self.env['purchase.order'].create({
  1103. 'currency_id': self.eur_currency.id,
  1104. 'partner_id': self.partner_id.id,
  1105. 'order_line': [
  1106. (0, 0, {
  1107. 'name': product_avg.name,
  1108. 'product_id': product_avg.id,
  1109. 'product_qty': 1.0,
  1110. 'product_uom': product_avg.uom_po_id.id,
  1111. 'price_unit': 30.0,
  1112. 'date_planned': date,
  1113. })
  1114. ],
  1115. })
  1116. po.button_confirm()
  1117. line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
  1118. picking = po.picking_ids
  1119. (picking.move_ids
  1120. .filtered(lambda l: l.purchase_line_id == line_product_avg)
  1121. .write({'quantity_done': 1.0}))
  1122. picking.button_validate()
  1123. # 5 Units received at rate 2 = 42.86
  1124. self.assertAlmostEqual(product_avg.standard_price, 60)
  1125. eur_rate.rate = 0.25
  1126. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  1127. 'move_type': 'in_invoice',
  1128. 'invoice_date': date,
  1129. 'date': date,
  1130. 'currency_id': self.eur_currency.id,
  1131. 'partner_id': self.partner_id.id,
  1132. 'invoice_line_ids': [
  1133. (0, 0, {
  1134. 'name': product_avg.name,
  1135. 'price_unit': 30.0,
  1136. 'product_id': product_avg.id,
  1137. 'purchase_line_id': line_product_avg.id,
  1138. 'quantity': 1.0,
  1139. 'account_id': self.stock_input_account.id,
  1140. 'tax_ids': [],
  1141. })
  1142. ]
  1143. })
  1144. self.env['stock.move'].invalidate_model()
  1145. inv.action_post()
  1146. self.assertRecordValues(inv.line_ids, [
  1147. # pylint: disable=C0326
  1148. {'balance': 120.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id},
  1149. {'balance': -120.0, 'amount_currency': -30.0, 'account_id': self.company_data['default_account_payable'].id},
  1150. ])
  1151. self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids.sorted('id'), [
  1152. # pylint: disable=C0326
  1153. {'balance': -60.0, 'amount_currency': -30.0, 'account_id': self.stock_input_account.id},
  1154. {'balance': 120.0, 'amount_currency': 30.0, 'account_id': self.stock_input_account.id},
  1155. {'balance': -60.0, 'amount_currency': 0.0, 'account_id': self.stock_input_account.id},
  1156. ])
  1157. input_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  1158. self.assertEqual(len(input_aml), 3)
  1159. def test_average_realtime_with_two_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
  1160. """
  1161. The PO and invoice are in the same foreign currency.
  1162. The deliveries occur at different times and rates
  1163. The invoice is created at an even different date
  1164. This should create a price difference entry.
  1165. """
  1166. company = self.env.user.company_id
  1167. company.anglo_saxon_accounting = True
  1168. company.currency_id = self.usd_currency
  1169. date_po = '2019-01-01'
  1170. date_delivery = '2019-01-08'
  1171. date_delivery1 = '2019-01-16'
  1172. date_invoice = '2019-01-10'
  1173. date_invoice1 = '2019-01-20'
  1174. self.product1.categ_id.property_valuation = 'real_time'
  1175. self.product1.categ_id.property_cost_method = 'average'
  1176. product_avg = self.product1_copy
  1177. product_avg.write({
  1178. 'purchase_method': 'purchase',
  1179. 'name': 'AVG',
  1180. 'standard_price': 0,
  1181. })
  1182. # SetUp currency and rates
  1183. self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
  1184. self.env['res.currency.rate'].search([]).unlink()
  1185. self.env['res.currency.rate'].create([{
  1186. 'name': date_po,
  1187. 'rate': 1.0,
  1188. 'currency_id': self.usd_currency.id,
  1189. 'company_id': company.id,
  1190. }, {
  1191. 'name': date_po,
  1192. 'rate': 1.5,
  1193. 'currency_id': self.eur_currency.id,
  1194. 'company_id': company.id,
  1195. }, {
  1196. 'name': date_delivery,
  1197. 'rate': 0.7,
  1198. 'currency_id': self.eur_currency.id,
  1199. 'company_id': company.id,
  1200. }, {
  1201. 'name': date_delivery1,
  1202. 'rate': 0.8,
  1203. 'currency_id': self.eur_currency.id,
  1204. 'company_id': company.id,
  1205. }, {
  1206. 'name': date_invoice,
  1207. 'rate': 2,
  1208. 'currency_id': self.eur_currency.id,
  1209. 'company_id': company.id,
  1210. }, {
  1211. 'name': date_invoice1,
  1212. 'rate': 2.2,
  1213. 'currency_id': self.eur_currency.id,
  1214. 'company_id': company.id,
  1215. }])
  1216. # Proceed
  1217. with freeze_time(date_po):
  1218. po = self.env['purchase.order'].create({
  1219. 'currency_id': self.eur_currency.id,
  1220. 'partner_id': self.partner_id.id,
  1221. 'date_order': date_po,
  1222. 'order_line': [
  1223. (0, 0, {
  1224. 'name': product_avg.name,
  1225. 'product_id': product_avg.id,
  1226. 'product_qty': 10.0,
  1227. 'product_uom': product_avg.uom_po_id.id,
  1228. 'price_unit': 30.0,
  1229. 'date_planned': date_po,
  1230. })
  1231. ],
  1232. })
  1233. po.button_confirm()
  1234. line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
  1235. with freeze_time(date_delivery):
  1236. picking = po.picking_ids
  1237. (picking.move_ids
  1238. .filtered(lambda l: l.purchase_line_id == line_product_avg)
  1239. .write({'quantity_done': 5.0}))
  1240. picking.button_validate()
  1241. picking._action_done() # Create Backorder
  1242. # 5 Units received at rate 0.7 = 42.86
  1243. self.assertAlmostEqual(product_avg.standard_price, 42.86)
  1244. with freeze_time(date_invoice):
  1245. inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  1246. 'move_type': 'in_invoice',
  1247. 'invoice_date': date_invoice,
  1248. 'date': date_invoice,
  1249. 'currency_id': self.eur_currency.id,
  1250. 'partner_id': self.partner_id.id,
  1251. 'invoice_line_ids': [
  1252. (0, 0, {
  1253. 'name': product_avg.name,
  1254. 'price_unit': 20.0,
  1255. 'product_id': product_avg.id,
  1256. 'purchase_line_id': line_product_avg.id,
  1257. 'quantity': 5.0,
  1258. 'account_id': self.stock_input_account.id,
  1259. 'tax_ids': [],
  1260. })
  1261. ]
  1262. })
  1263. inv.action_post()
  1264. # 5 Units invoiced at rate 2 instead of 0.7 and price unit 20 = 10
  1265. self.assertAlmostEqual(product_avg.standard_price, 10)
  1266. with freeze_time(date_delivery1):
  1267. backorder_picking = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
  1268. (backorder_picking.move_ids
  1269. .filtered(lambda l: l.purchase_line_id == line_product_avg)
  1270. .write({'quantity_done': 5.0}))
  1271. backorder_picking.button_validate()
  1272. # 5 Units invoiced at rate 2 (10) + 5 Units received at rate 0.8 (37.50) = 23.75
  1273. self.assertAlmostEqual(product_avg.standard_price, 23.75)
  1274. with freeze_time(date_invoice1):
  1275. inv1 = self.env['account.move'].with_context(default_move_type='in_invoice').create({
  1276. 'move_type': 'in_invoice',
  1277. 'invoice_date': date_invoice1,
  1278. 'date': date_invoice1,
  1279. 'currency_id': self.eur_currency.id,
  1280. 'partner_id': self.partner_id.id,
  1281. 'invoice_line_ids': [
  1282. (0, 0, {
  1283. 'name': product_avg.name,
  1284. 'price_unit': 40.0,
  1285. 'product_id': product_avg.id,
  1286. 'purchase_line_id': line_product_avg.id,
  1287. 'quantity': 5.0,
  1288. 'account_id': self.stock_input_account.id,
  1289. 'tax_ids': [],
  1290. })
  1291. ]
  1292. })
  1293. inv1.action_post()
  1294. # 5 Units invoiced at rate 2 (10) + 5 Units invoiced at rate 2.2 and unit price 40 (18.18) = 14.09
  1295. self.assertAlmostEqual(product_avg.standard_price, 14.09)
  1296. ##########################
  1297. # Invoice 0 #
  1298. ##########################
  1299. self.assertRecordValues(inv.line_ids, [
  1300. # pylint: disable=C0326
  1301. {'balance': 50.0, 'amount_currency': 100.0, 'account_id': self.stock_input_account.id},
  1302. {'balance': -50.0, 'amount_currency': -100.0, 'account_id': self.company_data['default_account_payable'].id},
  1303. ])
  1304. ##########################
  1305. # Invoice 1 #
  1306. ##########################
  1307. self.assertRecordValues(inv1.line_ids, [
  1308. # pylint: disable=C0326
  1309. {'balance': 90.91, 'amount_currency': 200.0, 'account_id': self.stock_input_account.id},
  1310. {'balance': -90.91, 'amount_currency': -200.0, 'account_id': self.company_data['default_account_payable'].id},
  1311. ])
  1312. ##########################
  1313. # Reconcile #
  1314. ##########################
  1315. self.assertTrue(inv.line_ids.full_reconcile_id)
  1316. self.assertTrue(inv1.line_ids.full_reconcile_id)
  1317. self.assertRecordValues(inv.line_ids.full_reconcile_id.reconciled_line_ids.sorted('id'), [
  1318. # pylint: disable=C0326
  1319. {'balance': -214.29, 'amount_currency': -150.0},
  1320. {'balance': 50, 'amount_currency': 100},
  1321. {'balance': 164.29, 'amount_currency': 0.0},
  1322. {'balance': 0.00, 'amount_currency': 50}
  1323. ])
  1324. self.assertRecordValues(inv1.line_ids.full_reconcile_id.reconciled_line_ids.sorted('id'), [
  1325. # pylint: disable=C0326
  1326. {'balance': -187.5, 'amount_currency': -150.0},
  1327. {'balance': 90.91, 'amount_currency': 200.0},
  1328. {'balance': 96.59, 'amount_currency': 0.0},
  1329. {'balance': 0.00, 'amount_currency': -50.0},
  1330. ])
  1331. def test_anglosaxon_valuation_price_total_diff_discount(self):
  1332. """
  1333. PO: price unit: 110
  1334. Inv: price unit: 100
  1335. discount: 10
  1336. """
  1337. self.env.company.anglo_saxon_accounting = True
  1338. self.product1.categ_id.property_cost_method = 'fifo'
  1339. self.product1.categ_id.property_valuation = 'real_time'
  1340. # Create PO
  1341. po_form = Form(self.env['purchase.order'])
  1342. po_form.partner_id = self.partner_id
  1343. with po_form.order_line.new() as po_line:
  1344. po_line.product_id = self.product1
  1345. po_line.product_qty = 1
  1346. po_line.price_unit = 110.0
  1347. order = po_form.save()
  1348. order.button_confirm()
  1349. # Receive the goods
  1350. receipt = order.picking_ids[0]
  1351. receipt.move_ids.quantity_done = 1
  1352. receipt.button_validate()
  1353. # Create an invoice with a different price and a discount
  1354. invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  1355. invoice_form.invoice_date = invoice_form.date
  1356. invoice_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-order.id)
  1357. with invoice_form.invoice_line_ids.edit(0) as line_form:
  1358. line_form.price_unit = 100.0
  1359. line_form.discount = 10.0
  1360. invoice = invoice_form.save()
  1361. invoice.action_post()
  1362. # Check what was posted in the stock valuation account
  1363. stock_valuation_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  1364. self.assertEqual(
  1365. len(stock_valuation_aml), 2,
  1366. "Two lines for the stock valuation account: one from the receipt (debit 110) and one from the bill (credit 20)")
  1367. self.assertAlmostEqual(sum(stock_valuation_aml.mapped('debit')), 110)
  1368. self.assertAlmostEqual(sum(stock_valuation_aml.mapped('credit')), 20, msg="Credit of 20 because of the difference between the PO and its invoice")
  1369. # Check what was posted in stock input account
  1370. input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
  1371. self.assertEqual(len(input_aml), 3, "Only two lines should have been generated in stock input account: one when receiving the product, two when making the invoice.")
  1372. self.assertAlmostEqual(sum(input_aml.mapped('debit')), 110, msg="Total debit value on stock input account should be equal to the original PO price of the product.")
  1373. self.assertAlmostEqual(sum(input_aml.mapped('credit')), 110, msg="Total credit value on stock input account should be equal to the original PO price of the product.")
  1374. def test_anglosaxon_valuation_discount(self):
  1375. """
  1376. PO: price unit: 100
  1377. Inv: price unit: 100
  1378. discount: 10
  1379. """
  1380. self.env.company.anglo_saxon_accounting = True
  1381. self.product1.categ_id.property_cost_method = 'fifo'
  1382. self.product1.categ_id.property_valuation = 'real_time'
  1383. # Create PO
  1384. po_form = Form(self.env['purchase.order'])
  1385. po_form.partner_id = self.partner_id
  1386. with po_form.order_line.new() as po_line:
  1387. po_line.product_id = self.product1
  1388. po_line.product_qty = 1
  1389. po_line.price_unit = 100.0
  1390. order = po_form.save()
  1391. order.button_confirm()
  1392. # Receive the goods
  1393. receipt = order.picking_ids[0]
  1394. receipt.move_ids.quantity_done = 1
  1395. receipt.button_validate()
  1396. # Create an invoice with a different price and a discount
  1397. invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  1398. invoice_form.invoice_date = invoice_form.date
  1399. invoice_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-order.id)
  1400. with invoice_form.invoice_line_ids.edit(0) as line_form:
  1401. line_form.tax_ids.clear()
  1402. line_form.discount = 10.0
  1403. invoice = invoice_form.save()
  1404. invoice.action_post()
  1405. # Check what was posted in the stock valuation account
  1406. stock_valuation_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  1407. self.assertEqual(len(stock_valuation_aml), 2, "Only one line should have been generated in the price difference account.")
  1408. self.assertAlmostEqual(sum(stock_valuation_aml.mapped('debit')), 100)
  1409. self.assertAlmostEqual(sum(stock_valuation_aml.mapped('credit')), 10, msg="Credit of 10 because of the 10% discount")
  1410. # Check what was posted in stock input account
  1411. input_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  1412. self.assertEqual(len(input_aml), 3, "Three lines generated in stock input account: one when receiving the product, two when making the invoice.")
  1413. self.assertAlmostEqual(sum(input_aml.mapped('debit')), 100, msg="Total debit value on stock input account should be equal to the original PO price of the product.")
  1414. self.assertAlmostEqual(sum(input_aml.mapped('credit')), 100, msg="Total credit value on stock input account should be equal to the original PO price of the product.")
  1415. def test_anglosaxon_valuation_price_unit_diff_discount(self):
  1416. """
  1417. PO: price unit: 90
  1418. Inv: price unit: 100
  1419. discount: 10
  1420. """
  1421. self.env.company.anglo_saxon_accounting = True
  1422. self.product1.categ_id.property_cost_method = 'fifo'
  1423. self.product1.categ_id.property_valuation = 'real_time'
  1424. # Create PO
  1425. po_form = Form(self.env['purchase.order'])
  1426. po_form.partner_id = self.partner_id
  1427. with po_form.order_line.new() as po_line:
  1428. po_line.product_id = self.product1
  1429. po_line.product_qty = 1
  1430. po_line.price_unit = 90.0
  1431. order = po_form.save()
  1432. order.button_confirm()
  1433. # Receive the goods
  1434. receipt = order.picking_ids[0]
  1435. receipt.move_ids.quantity_done = 1
  1436. receipt.button_validate()
  1437. # Create an invoice with a different price and a discount
  1438. invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  1439. invoice_form.invoice_date = invoice_form.date
  1440. invoice_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-order.id)
  1441. with invoice_form.invoice_line_ids.edit(0) as line_form:
  1442. line_form.price_unit = 100.0
  1443. line_form.discount = 10.0
  1444. invoice = invoice_form.save()
  1445. invoice.action_post()
  1446. # Check if something was posted in the price difference account
  1447. price_diff_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  1448. self.assertEqual(price_diff_aml.debit, 90, "Should have only one line in the stock valuation account, created by the receipt.")
  1449. # Check what was posted in stock input account
  1450. input_aml = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  1451. self.assertEqual(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
  1452. self.assertAlmostEqual(sum(input_aml.mapped('debit')), 90, "Total debit value on stock input account should be equal to the original PO price of the product.")
  1453. self.assertAlmostEqual(sum(input_aml.mapped('credit')), 90, "Total credit value on stock input account should be equal to the original PO price of the product.")
  1454. def test_anglosaxon_valuation_price_unit_diff_avco(self):
  1455. """
  1456. Inv: price unit: 100
  1457. """
  1458. self.env.company.anglo_saxon_accounting = True
  1459. self.product1.categ_id.property_cost_method = 'average'
  1460. self.product1.categ_id.property_valuation = 'real_time'
  1461. self.product1.standard_price = 1.01
  1462. invoice = self.env['account.move'].create({
  1463. 'move_type': 'in_invoice',
  1464. 'invoice_date': '2022-03-31',
  1465. 'partner_id': self.partner_id.id,
  1466. 'invoice_line_ids': [
  1467. (0, 0, {'product_id': self.product1.id, 'quantity': 10.50, 'price_unit': 1.01, 'tax_ids': self.tax_purchase_a.ids})
  1468. ]
  1469. })
  1470. # Check if something was posted in the stock valuation account.
  1471. stock_val_aml = invoice.line_ids.filtered(lambda l: l.account_id == self.stock_valuation_account)
  1472. self.assertEqual(len(stock_val_aml), 0, "No line should have been generated in the stock valuation account.")
  1473. def test_price_diff_with_partial_bills_and_delivered_qties(self):
  1474. """
  1475. Fifo + Real time.
  1476. Default UoM of the product is Unit.
  1477. Company in USD. 1 USD = 2 EUR.
  1478. Receive 10 Hundred @ $50:
  1479. Receive 7 Hundred (R1)
  1480. Receive 3 Hundred (R2)
  1481. Deliver 5 Hundred
  1482. Bill
  1483. 1 Hundred @ 120€ -> already out
  1484. 3 Hundred @ 120€ -> already out
  1485. 2 Hundred @ 120€ -> one is out, the other is in the stock
  1486. 4 Hundred @ 120€ -> nothing out
  1487. When billing:
  1488. - The already-delivered qty should not generate any SVL for the
  1489. price difference and we should directly post some COGS entries
  1490. - The in-stock qty should generate an SVL for the price difference,
  1491. and we should post the journal entries related to that SVL
  1492. Deliver 2 Hundred
  1493. The SVL should include:
  1494. - 2 x 50 (cost by hundred)
  1495. - 2 x 10 (the price diff from step "Bill 2 Hundred @ 60" and "Bill
  1496. 4 Hundred @ 60")
  1497. """
  1498. expected_svl_values = [] # USD
  1499. warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
  1500. stock_location = warehouse.lot_stock_id
  1501. customer_location = self.env.ref('stock.stock_location_customers')
  1502. eur_curr = self.env.ref('base.EUR')
  1503. self.env['res.currency.rate'].create({
  1504. 'name': fields.Date.today(),
  1505. 'company_id': self.env.company.id,
  1506. 'currency_id': eur_curr.id,
  1507. 'rate': 2,
  1508. })
  1509. grp_uom = self.env.ref('uom.group_uom')
  1510. self.env.user.write({'groups_id': [(4, grp_uom.id)]})
  1511. uom_unit = self.env.ref('uom.product_uom_unit')
  1512. uom_hundred = self.env['uom.uom'].create({
  1513. 'name': '100 x U',
  1514. 'category_id': uom_unit.category_id.id,
  1515. 'ratio': 100.0,
  1516. 'uom_type': 'bigger',
  1517. 'rounding': uom_unit.rounding,
  1518. })
  1519. self.product1.write({
  1520. 'uom_id': uom_unit.id,
  1521. 'uom_po_id': uom_unit.id,
  1522. })
  1523. self.product1.categ_id.property_cost_method = 'fifo'
  1524. self.product1.categ_id.property_valuation = 'real_time'
  1525. po_form = Form(self.env['purchase.order'])
  1526. po_form.partner_id = self.partner_id
  1527. with po_form.order_line.new() as po_line:
  1528. po_line.product_id = self.product1
  1529. po_line.product_qty = 10
  1530. po_line.product_uom = uom_hundred
  1531. po_line.price_unit = 50.0
  1532. po = po_form.save()
  1533. po.button_confirm()
  1534. # Receive 7 Hundred
  1535. receipt01 = po.picking_ids[0]
  1536. receipt01.move_ids.move_line_ids.qty_done = 700
  1537. action = receipt01.button_validate()
  1538. backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(action['context'])).save()
  1539. backorder_wizard.process()
  1540. expected_svl_values += [7 * 50]
  1541. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1542. # Receive 3 Hundred
  1543. receipt02 = receipt01.backorder_ids
  1544. receipt02.move_ids._set_quantities_to_reservation()
  1545. receipt02.button_validate()
  1546. expected_svl_values += [3 * 50]
  1547. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1548. # Delivery 5 Hundred
  1549. delivery01 = self.env['stock.picking'].create({
  1550. 'location_id': stock_location.id,
  1551. 'location_dest_id': customer_location.id,
  1552. 'picking_type_id': warehouse.out_type_id.id,
  1553. 'move_ids': [(0, 0, {
  1554. 'name': self.product1.name,
  1555. 'product_id': self.product1.id,
  1556. 'product_uom_qty': 5,
  1557. 'product_uom': uom_hundred.id,
  1558. 'location_id': stock_location.id,
  1559. 'location_dest_id': customer_location.id,
  1560. })],
  1561. })
  1562. delivery01.action_confirm()
  1563. delivery01.move_ids._set_quantities_to_reservation()
  1564. delivery01.button_validate()
  1565. expected_svl_values += [-5 * 50]
  1566. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1567. # We will create a price diff SVL only for the remaining quantities not yet billed
  1568. # On the bill, price unit is 120€, i.e. $60 -> price diff equal to $10
  1569. expense_account = self.company_data['default_account_expense']
  1570. valuation_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id)])
  1571. expense_amls = self.env['account.move.line'].search([('account_id', '=', expense_account.id)])
  1572. input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id)])
  1573. bills = self.env['account.move']
  1574. # pylint: disable=bad-whitespace
  1575. for qty, new_svl_expected, expected_valuations, expected_expenses in [
  1576. (1, [], [], [10.0]), # 1 hundred already out
  1577. (3, [], [], [30.0]), # 3 hundred already out
  1578. (2, [1 * 10.0], [10.0], [10.0]), # 1 hundred already out and 1 hundred in stock (from R1)
  1579. (4, [1 * 10.0, 3 * 10.0], [3 * 10.0, 1 * 10.0], []), # 4 hundred in stock, 1 from R1 and 3 from R2
  1580. ]:
  1581. bill_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  1582. bill_form.invoice_date = bill_form.date
  1583. bill_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po.id)
  1584. bill = bill_form.save()
  1585. bill.invoice_line_ids.quantity = qty
  1586. bill.invoice_line_ids.price_unit = 120.0
  1587. bill.invoice_line_ids.product_uom_id = uom_hundred
  1588. bill.currency_id = eur_curr
  1589. bill.action_post()
  1590. bills |= bill
  1591. err_msg = 'Incorrect while billing %s hundred' % qty
  1592. # stock side
  1593. expected_svl_values += new_svl_expected
  1594. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values, err_msg)
  1595. # account side
  1596. new_valuation_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_valuation_account.id), ('id', 'not in', valuation_amls.ids)])
  1597. new_expense_amls = self.env['account.move.line'].search([('account_id', '=', expense_account.id), ('id', 'not in', expense_amls.ids)])
  1598. new_input_amls = self.env['account.move.line'].search([('account_id', '=', self.stock_input_account.id), ('id', 'not in', input_amls.ids)])
  1599. valuation_amls |= new_valuation_amls
  1600. expense_amls |= new_expense_amls
  1601. input_amls |= new_input_amls
  1602. self.assertEqual(new_valuation_amls.mapped('debit'), expected_valuations, err_msg)
  1603. self.assertEqual(new_expense_amls.mapped('debit'), expected_expenses, err_msg)
  1604. self.assertEqual(new_input_amls.filtered(lambda aml: aml.credit > 0).mapped('credit'), expected_expenses + expected_valuations, err_msg)
  1605. self.assertEqual(new_input_amls.filtered(lambda aml: aml.debit > 0).debit, qty * 60, err_msg)
  1606. # All AML of Stock Interim Receipt should be reconciled
  1607. input_amls = bills.line_ids.filtered(lambda aml: aml.account_id == self.stock_input_account)
  1608. full_reconcile = input_amls[0].full_reconcile_id
  1609. self.assertTrue(full_reconcile)
  1610. self.assertTrue(all(aml.full_reconcile_id == full_reconcile for aml in input_amls))
  1611. # Delivery 2 Hundred
  1612. delivery02 = self.env['stock.picking'].create({
  1613. 'location_id': stock_location.id,
  1614. 'location_dest_id': customer_location.id,
  1615. 'picking_type_id': warehouse.out_type_id.id,
  1616. 'move_ids': [(0, 0, {
  1617. 'name': self.product1.name,
  1618. 'product_id': self.product1.id,
  1619. 'product_uom_qty': 2,
  1620. 'product_uom': uom_hundred.id,
  1621. 'location_id': stock_location.id,
  1622. 'location_dest_id': customer_location.id,
  1623. })],
  1624. })
  1625. delivery02.action_confirm()
  1626. delivery02.move_ids._set_quantities_to_reservation()
  1627. delivery02.button_validate()
  1628. expected_svl_values += [-2 * 50 + -2 * 10]
  1629. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1630. svl_r01, svl_r02, _svl_d01, svl_diff_01, svl_diff_02, svl_diff_03, _svl_d02 = self.product1.stock_valuation_layer_ids
  1631. self.assertEqual(svl_diff_01.stock_valuation_layer_id, svl_r01)
  1632. self.assertEqual(svl_diff_02.stock_valuation_layer_id, svl_r01)
  1633. self.assertEqual(svl_diff_03.stock_valuation_layer_id, svl_r02)
  1634. def test_partial_bills_and_reconciliation(self):
  1635. """
  1636. Fifo, Auto
  1637. Receive 5
  1638. Deliver 5
  1639. Bill 1 (with price diff)
  1640. Bill 4 (with price diff)
  1641. The lines in stock input account should be reconciled
  1642. """
  1643. self.product1.categ_id.property_cost_method = 'fifo'
  1644. self.product1.categ_id.property_valuation = 'real_time'
  1645. warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
  1646. stock_location = warehouse.lot_stock_id
  1647. customer_location = self.env.ref('stock.stock_location_customers')
  1648. po_form = Form(self.env['purchase.order'])
  1649. po_form.partner_id = self.partner_id
  1650. with po_form.order_line.new() as po_line:
  1651. po_line.product_id = self.product1
  1652. po_line.product_qty = 5
  1653. po_line.price_unit = 50.0
  1654. po = po_form.save()
  1655. po.button_confirm()
  1656. receipt = po.picking_ids[0]
  1657. receipt.move_ids._set_quantities_to_reservation()
  1658. receipt.button_validate()
  1659. delivery = self.env['stock.picking'].create({
  1660. 'location_id': stock_location.id,
  1661. 'location_dest_id': customer_location.id,
  1662. 'picking_type_id': warehouse.out_type_id.id,
  1663. 'move_ids': [(0, 0, {
  1664. 'name': self.product1.name,
  1665. 'product_id': self.product1.id,
  1666. 'product_uom_qty': 5,
  1667. 'location_id': stock_location.id,
  1668. 'location_dest_id': customer_location.id,
  1669. })],
  1670. })
  1671. delivery.action_confirm()
  1672. delivery.move_ids._set_quantities_to_reservation()
  1673. delivery.button_validate()
  1674. bill01_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  1675. bill01_form.invoice_date = bill01_form.date
  1676. bill01_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po.id)
  1677. bill01 = bill01_form.save()
  1678. bill01.invoice_line_ids.quantity = 1
  1679. bill01.invoice_line_ids.price_unit = 60
  1680. bill01.action_post()
  1681. bill02_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
  1682. bill02_form.invoice_date = bill02_form.date
  1683. bill02_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po.id)
  1684. bill02 = bill02_form.save()
  1685. bill02.invoice_line_ids.quantity = 4
  1686. bill02.invoice_line_ids.price_unit = 60
  1687. bill02.action_post()
  1688. input_amls = (bill01 + bill02).line_ids.filtered(lambda aml: aml.account_id == self.stock_input_account)
  1689. full_reconcile = input_amls[0].full_reconcile_id
  1690. self.assertTrue(full_reconcile)
  1691. self.assertTrue(all(aml.full_reconcile_id == full_reconcile for aml in input_amls))
  1692. def test_pdiff_and_credit_notes(self):
  1693. """
  1694. Auto FIFO
  1695. PO 12 @ 10
  1696. Receive with backorders: 4, 3 and then 5
  1697. Will generate 3 SVL
  1698. Bill:
  1699. BILL01: 3 @ 12
  1700. BILL02: 2 @ 11
  1701. BILL03: 1 @ 15
  1702. BILL04: 4 @ 9
  1703. BILL05: 2 @ 10
  1704. -: Refund 1 from BILL01
  1705. -: Refund all from BILL02
  1706. -: Refund 2 from BILL04
  1707. -: Refund 1 from BILL05
  1708. BILL06: 6 @ 18
  1709. """
  1710. self.product1.categ_id.property_cost_method = 'fifo'
  1711. self.product1.categ_id.property_valuation = 'real_time'
  1712. po_form = Form(self.env['purchase.order'])
  1713. po_form.partner_id = self.partner_id
  1714. with po_form.order_line.new() as po_line:
  1715. po_line.product_id = self.product1
  1716. po_line.product_qty = 12
  1717. po_line.price_unit = 10.0
  1718. po = po_form.save()
  1719. po.button_confirm()
  1720. receipt01 = po.picking_ids
  1721. receipt01.move_ids.move_line_ids.qty_done = 4
  1722. action = receipt01.button_validate()
  1723. backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(action['context'])).save()
  1724. backorder_wizard.process()
  1725. receipt02 = receipt01.backorder_ids
  1726. receipt02.move_ids.move_line_ids.qty_done = 3
  1727. action = receipt02.button_validate()
  1728. backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(action['context'])).save()
  1729. backorder_wizard.process()
  1730. receipt03 = receipt02.backorder_ids
  1731. receipt03.move_ids.move_line_ids.qty_done = 5
  1732. receipt03.button_validate()
  1733. expected_svl_values = [40, 30, 50]
  1734. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_values)
  1735. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1736. # pylint: disable=bad-whitespace
  1737. for qty, price, expected_svl_values, expected_svl_remaining_values in [
  1738. (3.0, 12.0, [40.0, 30.0, 50.0, 6.0], [46.0, 30.0, 50.0, 0.0]),
  1739. (2.0, 11.0, [40.0, 30.0, 50.0, 6.0, 1.0, 1.0], [47.0, 31.0, 50.0, 0.0, 0.0, 0.0]),
  1740. (1.0, 15.0, [40.0, 30.0, 50.0, 6.0, 1.0, 1.0, 5.0], [47.0, 36.0, 50.0, 0.0, 0.0, 0.0, 0.0]),
  1741. (4.0, 9.0, [40.0, 30.0, 50.0, 6.0, 1.0, 1.0, 5.0, -1.0, -3.0], [47.0, 35.0, 47.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
  1742. (2.0, 10.0, [40.0, 30.0, 50.0, 6.0, 1.0, 1.0, 5.0, -1.0, -3.0], [47.0, 35.0, 47.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
  1743. ]:
  1744. self._bill(po, qty, price)
  1745. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values, 'Err while invoicing %s @ %s' % (qty, price))
  1746. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values, 'Err while invoicing %s @ %s' % (qty, price))
  1747. bill01, bill02, _bill03, bill04, bill05 = po.invoice_ids.sorted('id')
  1748. self._refund(bill01, 1.0)
  1749. expected_svl_values += [-2.0]
  1750. expected_svl_remaining_values += [0.0]
  1751. # should impact the first layer
  1752. expected_svl_remaining_values[0] -= 2.0
  1753. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1754. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1755. self._refund(bill02)
  1756. expected_svl_values += [-1.0, -1.0]
  1757. expected_svl_remaining_values += [0.0, 0.0]
  1758. # should impact the two first layers
  1759. expected_svl_remaining_values[0] -= 1.0
  1760. expected_svl_remaining_values[1] -= 1.0
  1761. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1762. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1763. self._refund(bill04, 2.0)
  1764. expected_svl_values += [1.0, 1.0]
  1765. expected_svl_remaining_values += [0.0, 0.0]
  1766. # should impact the two last layers
  1767. expected_svl_remaining_values[1] += 1.0
  1768. expected_svl_remaining_values[2] += 1.0
  1769. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1770. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1771. self._refund(bill05, 1.0)
  1772. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1773. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1774. self._bill(po, price=18.0)
  1775. expected_svl_values += [16.0, 16.0, 16.0]
  1776. expected_svl_remaining_values += [0.0, 0.0, 0.0]
  1777. # should impact all layers
  1778. expected_svl_remaining_values[0] += 16.0
  1779. expected_svl_remaining_values[1] += 16.0
  1780. expected_svl_remaining_values[2] += 16.0
  1781. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1782. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1783. accounts = self.product1.product_tmpl_id._get_product_accounts()
  1784. stock_in_amls = self.env['account.move.line'].search([('account_id', '=', accounts['stock_input'].id)], order='id')
  1785. self.assertRecordValues(stock_in_amls, [
  1786. # Receipts
  1787. {'debit': 0.0, 'credit': 40.0},
  1788. {'debit': 0.0, 'credit': 30.0},
  1789. {'debit': 0.0, 'credit': 50.0},
  1790. # Bill01 3 @ 12
  1791. {'debit': 36.0, 'credit': 0.0},
  1792. {'debit': 0.0, 'credit': 6.0},
  1793. # Bill02 2 @ 11
  1794. {'debit': 22.0, 'credit': 0.0},
  1795. {'debit': 0.0, 'credit': 1.0},
  1796. {'debit': 0.0, 'credit': 1.0},
  1797. # Bill03 1 @ 15
  1798. {'debit': 15.0, 'credit': 0.0},
  1799. {'debit': 0.0, 'credit': 5.0},
  1800. # Bill04 4 @ 9
  1801. {'debit': 36.0, 'credit': 0.0},
  1802. {'debit': 1.0, 'credit': 0.0},
  1803. {'debit': 3.0, 'credit': 0.0},
  1804. # Bill05 2 @ 10
  1805. {'debit': 20.0, 'credit': 0.0},
  1806. # Refund 1 from BILL01
  1807. {'debit': 0.0, 'credit': 12.0},
  1808. {'debit': 2.0, 'credit': 0.0},
  1809. # Refund all from BILL02
  1810. {'debit': 0.0, 'credit': 22.0},
  1811. {'debit': 1.0, 'credit': 0.0},
  1812. {'debit': 1.0, 'credit': 0.0},
  1813. # Refund 2 from BILL04
  1814. {'debit': 0.0, 'credit': 18.0},
  1815. {'debit': 0.0, 'credit': 1.0},
  1816. {'debit': 0.0, 'credit': 1.0},
  1817. # Refund 1 from BILL05
  1818. {'debit': 0.0, 'credit': 10.0},
  1819. # BILL06: 6 @ 18
  1820. {'debit': 108.0, 'credit': 0.0},
  1821. {'debit': 0.0, 'credit': 16.0},
  1822. {'debit': 0.0, 'credit': 16.0},
  1823. {'debit': 0.0, 'credit': 16.0},
  1824. ])
  1825. self.assertTrue(all(aml.full_reconcile_id for aml in stock_in_amls))
  1826. def test_pdiff_with_credit_notes_and_delivered_qties(self):
  1827. """
  1828. Auto FIFO
  1829. IN 10 @ 10
  1830. Bill 10 @ 12
  1831. OUT 3
  1832. Full Refund
  1833. Bill 10 @ 9
  1834. OUT 1
  1835. Full Refund
  1836. Bill 10 @ 10
  1837. """
  1838. self.product1.categ_id.property_cost_method = 'fifo'
  1839. self.product1.categ_id.property_valuation = 'real_time'
  1840. expected_svl_values = []
  1841. expected_svl_remaining_values = []
  1842. warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
  1843. stock_location = warehouse.lot_stock_id
  1844. customer_location = self.env.ref('stock.stock_location_customers')
  1845. po_form = Form(self.env['purchase.order'])
  1846. po_form.partner_id = self.partner_id
  1847. with po_form.order_line.new() as po_line:
  1848. po_line.product_id = self.product1
  1849. po_line.product_qty = 10
  1850. po_line.price_unit = 10.0
  1851. po = po_form.save()
  1852. po.button_confirm()
  1853. receipt = po.picking_ids
  1854. receipt.move_ids.move_line_ids.qty_done = 10
  1855. receipt.button_validate()
  1856. expected_svl_values += [100.0]
  1857. expected_svl_remaining_values += [100.0]
  1858. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1859. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1860. bill01 = self._bill(po, price=12)
  1861. expected_svl_values += [20.0]
  1862. expected_svl_remaining_values += [0.0]
  1863. expected_svl_remaining_values[0] += 20.0 # should impact the layer of the receipt
  1864. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1865. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1866. delivery = self.env['stock.picking'].create({
  1867. 'location_id': stock_location.id,
  1868. 'location_dest_id': customer_location.id,
  1869. 'picking_type_id': warehouse.out_type_id.id,
  1870. 'move_ids': [(0, 0, {
  1871. 'name': self.product1.name,
  1872. 'product_id': self.product1.id,
  1873. 'product_uom_qty': 3,
  1874. 'product_uom': self.product1.uom_id.id,
  1875. 'location_id': stock_location.id,
  1876. 'location_dest_id': customer_location.id,
  1877. })],
  1878. })
  1879. delivery.action_confirm()
  1880. delivery.move_ids.quantity_done = 3.0
  1881. delivery.button_validate()
  1882. expected_svl_values += [-36.0]
  1883. expected_svl_remaining_values += [0.0]
  1884. expected_svl_remaining_values[0] -= 36.0 # should impact the layer of the receipt
  1885. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1886. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1887. self._refund(bill01)
  1888. expected_svl_values += [-14.0]
  1889. expected_svl_remaining_values += [0.0]
  1890. expected_svl_remaining_values[0] -= 14.0 # should impact the layer of the receipt
  1891. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1892. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1893. bill02 = self._bill(po, price=9)
  1894. expected_svl_values += [-7.0]
  1895. expected_svl_remaining_values += [0.0]
  1896. expected_svl_remaining_values[0] -= 7.0 # should impact the layer of the receipt
  1897. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1898. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1899. delivery = self.env['stock.picking'].create({
  1900. 'location_id': stock_location.id,
  1901. 'location_dest_id': customer_location.id,
  1902. 'picking_type_id': warehouse.out_type_id.id,
  1903. 'move_ids': [(0, 0, {
  1904. 'name': self.product1.name,
  1905. 'product_id': self.product1.id,
  1906. 'product_uom_qty': 1,
  1907. 'product_uom': self.product1.uom_id.id,
  1908. 'location_id': stock_location.id,
  1909. 'location_dest_id': customer_location.id,
  1910. })],
  1911. })
  1912. delivery.action_confirm()
  1913. delivery.move_ids.quantity_done = 1.0
  1914. delivery.button_validate()
  1915. expected_svl_values += [-9.0]
  1916. expected_svl_remaining_values += [0.0]
  1917. expected_svl_remaining_values[0] -= 9.0 # should impact the layer of the receipt
  1918. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1919. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1920. self._refund(bill02)
  1921. expected_svl_values += [6.0]
  1922. expected_svl_remaining_values += [0.0]
  1923. expected_svl_remaining_values[0] += 6.0 # should impact the layer of the receipt
  1924. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1925. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1926. self._bill(po)
  1927. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1928. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1929. accounts = self.product1.product_tmpl_id._get_product_accounts()
  1930. stock_in_amls = self.env['account.move.line'].search([('account_id', '=', accounts['stock_input'].id)], order='id')
  1931. self.assertRecordValues(stock_in_amls, [
  1932. # IN 10 @ 10
  1933. {'debit': 0.0, 'credit': 100.0},
  1934. # Bill 10 @ 12
  1935. {'debit': 120.0, 'credit': 0.0},
  1936. {'debit': 0.0, 'credit': 20.0},
  1937. # (OUT 3)
  1938. # Refund: here, we skip the 3 products delivered in the meantime,
  1939. # i.e. we only compensate the on-hand quantity (hence the $14
  1940. # instead of $20)
  1941. {'debit': 0.0, 'credit': 120.0},
  1942. {'debit': 14.0, 'credit': 0.0},
  1943. # Bill 10 @ 9
  1944. {'debit': 90.0, 'credit': 0.0},
  1945. {'debit': 3.0, 'credit': 0.0},
  1946. {'debit': 7.0, 'credit': 0.0},
  1947. # (OUT 1)
  1948. # Refund: again, we skip the product delivered in the meantime.
  1949. # We have $3 which is the reversing entry of the inital bill
  1950. # and 6$ from the stock valuation correction
  1951. {'debit': 0.0, 'credit': 90.0},
  1952. {'debit': 0.0, 'credit': 3.0},
  1953. {'debit': 0.0, 'credit': 6.0},
  1954. # Bill 10 @ 10
  1955. {'debit': 100.0, 'credit': 0},
  1956. ])
  1957. self.assertEqual(sum(stock_in_amls.mapped('debit')) - sum(stock_in_amls.mapped('credit')), -5,
  1958. "There should be a difference because of the skipped products while posting the refunds (see "
  1959. "comments in above `assertRecordValues`). The value is the sum of the price differences of each "
  1960. "delivery: 3 * $2 + 1 * $-1). The user will have to manually add some account entries to "
  1961. "balance the accounts")
  1962. def test_pdiff_with_returns_and_credit_notes(self):
  1963. """
  1964. Auto FIFO
  1965. IN 10 @ 10
  1966. Return 3
  1967. IN (Return) 3
  1968. Bill 10 @ 12
  1969. This step will impact 7 products of the first layer and 3 products
  1970. of the last one (i.e. the second IN)
  1971. Return 1
  1972. Refund 1 (from PO)
  1973. Return 5
  1974. Refund 5 (from initial bill)
  1975. """
  1976. self.product1.categ_id.property_cost_method = 'fifo'
  1977. self.product1.categ_id.property_valuation = 'real_time'
  1978. accounts = self.product1.product_tmpl_id._get_product_accounts()
  1979. expected_svl_values = []
  1980. expected_svl_remaining_values = []
  1981. po_form = Form(self.env['purchase.order'])
  1982. po_form.partner_id = self.partner_id
  1983. with po_form.order_line.new() as po_line:
  1984. po_line.product_id = self.product1
  1985. po_line.product_qty = 10
  1986. po_line.price_unit = 10.0
  1987. po = po_form.save()
  1988. po.button_confirm()
  1989. receipt = po.picking_ids
  1990. receipt.move_ids.move_line_ids.qty_done = 10
  1991. receipt.button_validate()
  1992. expected_svl_values += [100.0]
  1993. expected_svl_remaining_values += [100.0]
  1994. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  1995. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  1996. return01 = self._return(receipt, qty=3)
  1997. expected_svl_values += [-30.0]
  1998. expected_svl_remaining_values += [0.0]
  1999. expected_svl_remaining_values[0] -= 30
  2000. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  2001. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  2002. self._return(return01)
  2003. expected_svl_values += [30.0]
  2004. expected_svl_remaining_values += [30.0]
  2005. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  2006. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  2007. bill = self._bill(po, price=12)
  2008. # two new layers because we have 7 remaining products in the first in-layer and 3 in the second one
  2009. expected_svl_values += [14.0, 6.0]
  2010. expected_svl_remaining_values += [0.0, 0.0]
  2011. expected_svl_remaining_values[0] += 14.0
  2012. # `expected_svl_remaining_values[1]` is the return, it does not change
  2013. expected_svl_remaining_values[2] += 6.0
  2014. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  2015. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  2016. stock_in_amls = self.env['account.move.line'].search([('account_id', '=', accounts['stock_input'].id)])
  2017. self.assertTrue(stock_in_amls)
  2018. self.assertTrue(all(aml.full_reconcile_id for aml in stock_in_amls))
  2019. self._return(receipt, qty=1)
  2020. expected_svl_values += [-12.0]
  2021. expected_svl_remaining_values += [0.0]
  2022. expected_svl_remaining_values[0] -= 12.0
  2023. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  2024. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  2025. refund = self._bill(po, price=12)
  2026. self.assertEqual(refund.move_type, 'in_refund')
  2027. stock_in_amls = self.env['account.move.line'].search([('account_id', '=', accounts['stock_input'].id)])
  2028. self.assertTrue(stock_in_amls)
  2029. self.assertTrue(all(aml.full_reconcile_id for aml in stock_in_amls))
  2030. self._return(receipt, qty=5)
  2031. expected_svl_values += [-60.0]
  2032. expected_svl_remaining_values += [0.0]
  2033. expected_svl_remaining_values[0] -= 60.0
  2034. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  2035. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  2036. self._refund(bill, qty=5)
  2037. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
  2038. self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('remaining_value'), expected_svl_remaining_values)
  2039. stock_in_amls = self.env['account.move.line'].search([('account_id', '=', accounts['stock_input'].id)], order='id')
  2040. self.assertRecordValues(stock_in_amls, [
  2041. # IN 10 @ 10
  2042. {'debit': 0.0, 'credit': 100.0},
  2043. # Return 3
  2044. {'debit': 30.0, 'credit': 0.0},
  2045. # IN (Return) 3
  2046. {'debit': 0.0, 'credit': 30.0},
  2047. # Bill 10 @ 12
  2048. {'debit': 120, 'credit': 0.0},
  2049. {'debit': 0.0, 'credit': 14.0},
  2050. {'debit': 0.0, 'credit': 6.0},
  2051. # Return 1
  2052. {'debit': 12.0, 'credit': 0.0},
  2053. # Refund 1
  2054. {'debit': 0, 'credit': 12.0},
  2055. # Return 5
  2056. {'debit': 60.0, 'credit': 0.0},
  2057. # Refund 5
  2058. {'debit': 0.0, 'credit': 60.0},
  2059. ])
  2060. self.assertTrue(all(aml.full_reconcile_id for aml in stock_in_amls))
  2061. def _test_pdiff_and_order_between_bills_common(self):
  2062. self.product1.categ_id.property_cost_method = 'fifo'
  2063. self.product1.categ_id.property_valuation = 'real_time'
  2064. po_form = Form(self.env['purchase.order'])
  2065. po_form.partner_id = self.partner_id
  2066. with po_form.order_line.new() as po_line:
  2067. po_line.product_id = self.product1
  2068. po_line.product_qty = 2
  2069. po_line.price_unit = 10.0
  2070. po = po_form.save()
  2071. po.button_confirm()
  2072. receipt01 = po.picking_ids
  2073. receipt01.move_ids.move_line_ids.qty_done = 1
  2074. action = receipt01.button_validate()
  2075. backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(action['context'])).save()
  2076. backorder_wizard.process()
  2077. receipt02 = receipt01.backorder_ids
  2078. receipt02.move_ids.move_line_ids.qty_done = 1
  2079. receipt02.button_validate()
  2080. bill01 = self._bill(po, 1.0, 11)
  2081. ctx = {'active_ids': bill01.ids, 'active_id': bill01.id, 'active_model': 'account.move'}
  2082. credit_note_wizard = self.env['account.move.reversal'].with_context(ctx).create({
  2083. 'refund_method': "refund",
  2084. 'journal_id': bill01.journal_id.id,
  2085. })
  2086. refund = self.env['account.move'].browse(credit_note_wizard.reverse_moves()['res_id'])
  2087. action = po.action_create_invoice()
  2088. bill02 = self.env["account.move"].browse(action["res_id"])
  2089. bill02.invoice_date = fields.Date.today()
  2090. bill02.invoice_line_ids.quantity = 1.0
  2091. bill02.invoice_line_ids.price_unit = 12
  2092. return po, refund, bill02
  2093. def test_pdiff_and_order_between_bills_01(self):
  2094. """
  2095. Auto fifo
  2096. IN 1 @ 10 -> SVL01
  2097. IN 1 @ 10 -> SVL02
  2098. BILL01 1 @ 11
  2099. Should impact SVL01
  2100. Create draft Refund
  2101. Create draft BILL02 1 @ 12
  2102. Post Refund
  2103. Post BILL02
  2104. Should impact SVL01
  2105. Bill03 1 @ 13
  2106. Should impact SVL02
  2107. """
  2108. po, refund, bill02 = self._test_pdiff_and_order_between_bills_common()
  2109. refund.action_post()
  2110. bill02.action_post()
  2111. self._bill(po, price=13)
  2112. svls = self.product1.stock_valuation_layer_ids
  2113. self.assertEqual(svls.mapped('remaining_value'), [12.0, 13.0, 0.0, 0.0, 0.0, 0.0])
  2114. self.assertEqual(svls.mapped('value'), [10.0, 10.0, 1.0, -1.0, 2.0, 3.0])
  2115. def test_pdiff_and_order_between_bills_02(self):
  2116. """
  2117. Auto fifo
  2118. IN 1 @ 10 -> SVL01
  2119. IN 1 @ 10 -> SVL02
  2120. BILL01 1 @ 11
  2121. Should impact SVL01
  2122. Create draft Refund
  2123. Create draft BILL02 1 @ 12
  2124. Post BILL02
  2125. Should impact SVL02
  2126. Post Refund
  2127. Bill03 1 @ 13
  2128. Should impact SVL01
  2129. """
  2130. po, refund, bill02 = self._test_pdiff_and_order_between_bills_common()
  2131. bill02.action_post()
  2132. refund.action_post()
  2133. self._bill(po, price=13)
  2134. svls = self.product1.stock_valuation_layer_ids
  2135. self.assertEqual(svls.mapped('remaining_value'), [13.0, 12.0, 0.0, 0.0, 0.0, 0.0])
  2136. self.assertEqual(svls.mapped('value'), [10.0, 10.0, 1.0, 2.0, -1.0, 3.0])
  2137. def test_invoice_on_ordered_qty_with_backorder_and_different_currency_automated(self):
  2138. """Create a PO with currency different from the company currency. Set the
  2139. product to be invoiced on ordered quantities. Receive partially the products
  2140. and create a backorder. Create an invoice for the ordered quantity. Then
  2141. receive the backorder. Check if the valuation layer is correctly created.
  2142. """
  2143. usd_currency = self.env.ref('base.USD')
  2144. self.env.company.currency_id = usd_currency.id
  2145. self.product1.categ_id.property_cost_method = 'fifo'
  2146. self.product1.categ_id.property_valuation = 'real_time'
  2147. self.product1.purchase_method = 'purchase'
  2148. price_unit_EUR = 100
  2149. price_unit_USD = self.env.ref('base.EUR')._convert(price_unit_EUR, usd_currency, self.env.company, fields.Date.today(), round=False)
  2150. po = self.env['purchase.order'].create({
  2151. 'partner_id': self.partner_id.id,
  2152. 'currency_id': self.env.ref('base.EUR').id,
  2153. 'order_line': [
  2154. (0, 0, {
  2155. 'name': self.product1.name,
  2156. 'product_id': self.product1.id,
  2157. 'product_qty': 12.0,
  2158. 'product_uom': self.product1.uom_po_id.id,
  2159. 'price_unit': 100.0,
  2160. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  2161. }),
  2162. ],
  2163. })
  2164. po.button_confirm()
  2165. picking = po.picking_ids[0]
  2166. move = picking.move_ids[0]
  2167. move.quantity_done = 10
  2168. res_dict = picking.button_validate()
  2169. self.assertEqual(res_dict['res_model'], 'stock.backorder.confirmation')
  2170. wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')).with_context(res_dict['context'])
  2171. wizard.process()
  2172. self.assertAlmostEqual(move.stock_valuation_layer_ids.unit_cost, price_unit_USD)
  2173. po.action_create_invoice()
  2174. picking2 = po.picking_ids.filtered(lambda p: p.backorder_id)
  2175. move2 = picking2.move_ids[0]
  2176. move2.quantity_done = 2
  2177. picking2.button_validate()
  2178. self.assertAlmostEqual(move2.stock_valuation_layer_ids.unit_cost, price_unit_USD)
  2179. def test_invoice_on_ordered_qty_with_backorder_and_different_currency_manual(self):
  2180. """Same test as test_invoice_on_ordered_qty_with_backorder_and_different_currency_automated with manual_periodic valuation
  2181. Ensure that the absence of account_move_id on the layers does not generate an Exception
  2182. """
  2183. usd_currency = self.env.ref('base.USD')
  2184. self.env.company.currency_id = usd_currency.id
  2185. self.product1.categ_id.property_cost_method = 'fifo'
  2186. self.product1.categ_id.property_valuation = 'manual_periodic'
  2187. self.product1.purchase_method = 'purchase'
  2188. price_unit_EUR = 100
  2189. price_unit_USD = self.env.ref('base.EUR')._convert(price_unit_EUR, usd_currency, self.env.company, fields.Date.today(), round=False)
  2190. po = self.env['purchase.order'].create({
  2191. 'partner_id': self.partner_id.id,
  2192. 'currency_id': self.env.ref('base.EUR').id,
  2193. 'order_line': [
  2194. (0, 0, {
  2195. 'name': self.product1.name,
  2196. 'product_id': self.product1.id,
  2197. 'product_qty': 12.0,
  2198. 'product_uom': self.product1.uom_po_id.id,
  2199. 'price_unit': 100.0,
  2200. 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
  2201. }),
  2202. ],
  2203. })
  2204. po.button_confirm()
  2205. picking = po.picking_ids[0]
  2206. move = picking.move_ids[0]
  2207. move.quantity_done = 10
  2208. res_dict = picking.button_validate()
  2209. self.assertEqual(res_dict['res_model'], 'stock.backorder.confirmation')
  2210. wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')).with_context(res_dict['context'])
  2211. wizard.process()
  2212. self.assertAlmostEqual(move.stock_valuation_layer_ids.unit_cost, price_unit_USD)
  2213. po.action_create_invoice()
  2214. picking2 = po.picking_ids.filtered(lambda p: p.backorder_id)
  2215. move2 = picking2.move_ids[0]
  2216. move2.quantity_done = 2
  2217. picking2.button_validate()
  2218. self.assertAlmostEqual(move2.stock_valuation_layer_ids.unit_cost, price_unit_USD)