account_move_line.py 137 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789
  1. import ast
  2. from collections import defaultdict
  3. from contextlib import contextmanager
  4. from datetime import date, timedelta
  5. from functools import lru_cache
  6. from odoo import api, fields, models, Command, _
  7. from odoo.exceptions import ValidationError, UserError
  8. from odoo.tools import frozendict, formatLang, format_date, float_compare, Query
  9. from odoo.tools.sql import create_index
  10. from odoo.addons.web.controllers.utils import clean_action
  11. from odoo.addons.account.models.account_move import MAX_HASH_VERSION
  12. class AccountMoveLine(models.Model):
  13. _name = "account.move.line"
  14. _inherit = "analytic.mixin"
  15. _description = "Journal Item"
  16. _order = "date desc, move_name desc, id"
  17. _check_company_auto = True
  18. _rec_names_search = ['name', 'move_id', 'product_id']
  19. # ==============================================================================================
  20. # JOURNAL ENTRY
  21. # ==============================================================================================
  22. # === Parent fields === #
  23. move_id = fields.Many2one(
  24. comodel_name='account.move',
  25. string='Journal Entry',
  26. required=True,
  27. readonly=True,
  28. index=True,
  29. auto_join=True,
  30. ondelete="cascade",
  31. check_company=True,
  32. )
  33. journal_id = fields.Many2one(
  34. related='move_id.journal_id', store=True, precompute=True,
  35. index=True,
  36. copy=False,
  37. )
  38. company_id = fields.Many2one(
  39. related='move_id.company_id', store=True, readonly=True, precompute=True,
  40. index=True,
  41. )
  42. company_currency_id = fields.Many2one(
  43. string='Company Currency',
  44. related='move_id.company_currency_id', readonly=True, store=True, precompute=True,
  45. )
  46. move_name = fields.Char(
  47. string='Number',
  48. related='move_id.name', store=True,
  49. index='btree',
  50. )
  51. parent_state = fields.Selection(related='move_id.state', store=True)
  52. date = fields.Date(
  53. related='move_id.date', store=True,
  54. copy=False,
  55. group_operator='min',
  56. )
  57. ref = fields.Char(
  58. related='move_id.ref', store=True,
  59. copy=False,
  60. index='trigram',
  61. )
  62. is_storno = fields.Boolean(
  63. string="Company Storno Accounting",
  64. related='move_id.is_storno',
  65. help="Utility field to express whether the journal item is subject to storno accounting",
  66. )
  67. sequence = fields.Integer(compute='_compute_sequence', store=True, readonly=False, precompute=True)
  68. move_type = fields.Selection(related='move_id.move_type')
  69. # === Accountable fields === #
  70. account_id = fields.Many2one(
  71. comodel_name='account.account',
  72. string='Account',
  73. compute='_compute_account_id', store=True, readonly=False, precompute=True,
  74. inverse='_inverse_account_id',
  75. index=True,
  76. auto_join=True,
  77. ondelete="cascade",
  78. domain="[('deprecated', '=', False), ('company_id', '=', company_id), ('is_off_balance', '=', False)]",
  79. check_company=True,
  80. tracking=True,
  81. )
  82. name = fields.Char(
  83. string='Label',
  84. compute='_compute_name', store=True, readonly=False, precompute=True,
  85. tracking=True,
  86. )
  87. debit = fields.Monetary(
  88. string='Debit',
  89. compute='_compute_debit_credit', inverse='_inverse_debit', store=True, precompute=True,
  90. currency_field='company_currency_id',
  91. )
  92. credit = fields.Monetary(
  93. string='Credit',
  94. compute='_compute_debit_credit', inverse='_inverse_credit', store=True, precompute=True,
  95. currency_field='company_currency_id',
  96. )
  97. balance = fields.Monetary(
  98. string='Balance',
  99. compute='_compute_balance', store=True, readonly=False, precompute=True,
  100. currency_field='company_currency_id',
  101. )
  102. cumulated_balance = fields.Monetary(
  103. string='Cumulated Balance',
  104. compute='_compute_cumulated_balance',
  105. currency_field='company_currency_id',
  106. exportable=False,
  107. help="Cumulated balance depending on the domain and the order chosen in the view.")
  108. currency_rate = fields.Float(
  109. compute='_compute_currency_rate',
  110. help="Currency rate from company currency to document currency.",
  111. )
  112. amount_currency = fields.Monetary(
  113. string='Amount in Currency',
  114. group_operator=None,
  115. compute='_compute_amount_currency', inverse='_inverse_amount_currency', store=True, readonly=False, precompute=True,
  116. help="The amount expressed in an optional other currency if it is a multi-currency entry.")
  117. currency_id = fields.Many2one(
  118. comodel_name='res.currency',
  119. string='Currency',
  120. compute='_compute_currency_id', store=True, readonly=False, precompute=True,
  121. required=True,
  122. )
  123. is_same_currency = fields.Boolean(compute='_compute_same_currency')
  124. partner_id = fields.Many2one(
  125. comodel_name='res.partner',
  126. string='Partner',
  127. compute='_compute_partner_id', inverse='_inverse_partner_id', store=True, readonly=False, precompute=True,
  128. ondelete='restrict',
  129. )
  130. # === Origin fields === #
  131. reconcile_model_id = fields.Many2one(
  132. comodel_name='account.reconcile.model',
  133. string="Reconciliation Model",
  134. copy=False,
  135. readonly=True,
  136. check_company=True,
  137. )
  138. payment_id = fields.Many2one(
  139. comodel_name='account.payment',
  140. string="Originator Payment",
  141. related='move_id.payment_id', store=True,
  142. auto_join=True,
  143. index='btree_not_null',
  144. help="The payment that created this entry")
  145. statement_line_id = fields.Many2one(
  146. comodel_name='account.bank.statement.line',
  147. string="Originator Statement Line",
  148. related='move_id.statement_line_id', store=True,
  149. auto_join=True,
  150. index='btree_not_null',
  151. help="The statement line that created this entry")
  152. statement_id = fields.Many2one(
  153. related='statement_line_id.statement_id', store=True,
  154. auto_join=True,
  155. index='btree_not_null',
  156. copy=False,
  157. help="The bank statement used for bank reconciliation")
  158. # === Tax fields === #
  159. tax_ids = fields.Many2many(
  160. comodel_name='account.tax',
  161. string="Taxes",
  162. compute='_compute_tax_ids', store=True, readonly=False, precompute=True,
  163. context={'active_test': False},
  164. check_company=True,
  165. )
  166. group_tax_id = fields.Many2one(
  167. comodel_name='account.tax',
  168. string="Originator Group of Taxes",
  169. index='btree_not_null',
  170. )
  171. tax_line_id = fields.Many2one(
  172. comodel_name='account.tax',
  173. string='Originator Tax',
  174. related='tax_repartition_line_id.tax_id', store=True, precompute=True,
  175. ondelete='restrict',
  176. help="Indicates that this journal item is a tax line")
  177. tax_group_id = fields.Many2one( # used in the widget tax-group-custom-field
  178. string='Originator tax group',
  179. related='tax_line_id.tax_group_id', store=True, precompute=True,
  180. )
  181. tax_base_amount = fields.Monetary(
  182. string="Base Amount",
  183. readonly=True,
  184. currency_field='company_currency_id',
  185. )
  186. tax_repartition_line_id = fields.Many2one(
  187. comodel_name='account.tax.repartition.line',
  188. string="Originator Tax Distribution Line",
  189. ondelete='restrict',
  190. readonly=True,
  191. check_company=True,
  192. help="Tax distribution line that caused the creation of this move line, if any")
  193. tax_tag_ids = fields.Many2many(
  194. string="Tags",
  195. comodel_name='account.account.tag',
  196. ondelete='restrict',
  197. context={'active_test': False},
  198. tracking=True,
  199. help="Tags assigned to this line by the tax creating it, if any. It determines its impact on financial reports.",
  200. )
  201. tax_audit = fields.Char(
  202. string="Tax Audit String",
  203. compute="_compute_tax_audit", store=True,
  204. help="Computed field, listing the tax grids impacted by this line, and the amount it applies to each of them.")
  205. # Technical field. True if the balance of this move line needs to be
  206. # inverted when computing its total for each tag (for sales invoices, for # example)
  207. tax_tag_invert = fields.Boolean(
  208. string="Invert Tags",
  209. compute='_compute_tax_tag_invert', store=True, readonly=False, copy=False,
  210. )
  211. # === Reconciliation fields === #
  212. amount_residual = fields.Monetary(
  213. string='Residual Amount',
  214. compute='_compute_amount_residual', store=True,
  215. currency_field='company_currency_id',
  216. help="The residual amount on a journal item expressed in the company currency.",
  217. )
  218. amount_residual_currency = fields.Monetary(
  219. string='Residual Amount in Currency',
  220. compute='_compute_amount_residual', store=True,
  221. help="The residual amount on a journal item expressed in its currency (possibly not the "
  222. "company currency).",
  223. )
  224. reconciled = fields.Boolean(compute='_compute_amount_residual', store=True)
  225. full_reconcile_id = fields.Many2one(
  226. comodel_name='account.full.reconcile',
  227. string="Matching",
  228. copy=False,
  229. index='btree_not_null',
  230. readonly=True,
  231. )
  232. matched_debit_ids = fields.One2many(
  233. comodel_name='account.partial.reconcile', inverse_name='credit_move_id',
  234. string='Matched Debits',
  235. readonly=True,
  236. help='Debit journal items that are matched with this journal item.',
  237. )
  238. matched_credit_ids = fields.One2many(
  239. comodel_name='account.partial.reconcile', inverse_name='debit_move_id',
  240. string='Matched Credits',
  241. readonly=True,
  242. help='Credit journal items that are matched with this journal item.',
  243. )
  244. matching_number = fields.Char(
  245. string="Matching #",
  246. compute='_compute_matching_number', store=True,
  247. help="Matching number for this line, 'P' if it is only partially reconcile, or the name of "
  248. "the full reconcile if it exists.",
  249. )
  250. is_account_reconcile = fields.Boolean(
  251. string='Account Reconcile',
  252. related='account_id.reconcile',
  253. )
  254. # === Related fields ===
  255. account_type = fields.Selection(
  256. related='account_id.account_type',
  257. string="Internal Type",
  258. )
  259. account_internal_group = fields.Selection(related='account_id.internal_group')
  260. account_root_id = fields.Many2one(
  261. related='account_id.root_id',
  262. string="Account Root",
  263. store=True,
  264. )
  265. # ==============================================================================================
  266. # INVOICE
  267. # ==============================================================================================
  268. display_type = fields.Selection(
  269. selection=[
  270. ('product', 'Product'),
  271. ('cogs', 'Cost of Goods Sold'),
  272. ('tax', 'Tax'),
  273. ('rounding', "Rounding"),
  274. ('payment_term', 'Payment Term'),
  275. ('line_section', 'Section'),
  276. ('line_note', 'Note'),
  277. ('epd', 'Early Payment Discount')
  278. ],
  279. compute='_compute_display_type', store=True, readonly=False, precompute=True,
  280. required=True,
  281. )
  282. product_id = fields.Many2one(
  283. comodel_name='product.product',
  284. string='Product',
  285. inverse='_inverse_product_id',
  286. ondelete='restrict',
  287. )
  288. product_uom_id = fields.Many2one(
  289. comodel_name='uom.uom',
  290. string='Unit of Measure',
  291. compute='_compute_product_uom_id', store=True, readonly=False, precompute=True,
  292. domain="[('category_id', '=', product_uom_category_id)]",
  293. ondelete="restrict",
  294. )
  295. product_uom_category_id = fields.Many2one(
  296. comodel_name='uom.category',
  297. related='product_id.uom_id.category_id',
  298. )
  299. quantity = fields.Float(
  300. string='Quantity',
  301. compute='_compute_quantity', store=True, readonly=False, precompute=True,
  302. digits='Product Unit of Measure',
  303. help="The optional quantity expressed by this line, eg: number of product sold. "
  304. "The quantity is not a legal requirement but is very useful for some reports.",
  305. )
  306. date_maturity = fields.Date(
  307. string='Due Date',
  308. index=True,
  309. tracking=True,
  310. help="This field is used for payable and receivable journal entries. "
  311. "You can put the limit date for the payment of this line.",
  312. )
  313. # === Price fields === #
  314. price_unit = fields.Float(
  315. string='Unit Price',
  316. compute="_compute_price_unit", store=True, readonly=False, precompute=True,
  317. digits='Product Price',
  318. )
  319. price_subtotal = fields.Monetary(
  320. string='Subtotal',
  321. compute='_compute_totals', store=True,
  322. currency_field='currency_id',
  323. )
  324. price_total = fields.Monetary(
  325. string='Total',
  326. compute='_compute_totals', store=True,
  327. currency_field='currency_id',
  328. )
  329. discount = fields.Float(
  330. string='Discount (%)',
  331. digits='Discount',
  332. default=0.0,
  333. )
  334. # === Invoice sync fields === #
  335. term_key = fields.Binary(compute='_compute_term_key', exportable=False)
  336. tax_key = fields.Binary(compute='_compute_tax_key', exportable=False)
  337. compute_all_tax = fields.Binary(compute='_compute_all_tax', exportable=False)
  338. compute_all_tax_dirty = fields.Boolean(compute='_compute_all_tax')
  339. epd_key = fields.Binary(compute='_compute_epd_key', exportable=False)
  340. epd_needed = fields.Binary(compute='_compute_epd_needed', exportable=False)
  341. epd_dirty = fields.Boolean(compute='_compute_epd_needed')
  342. # === Analytic fields === #
  343. analytic_line_ids = fields.One2many(
  344. comodel_name='account.analytic.line', inverse_name='move_line_id',
  345. string='Analytic lines',
  346. )
  347. analytic_distribution = fields.Json(
  348. inverse="_inverse_analytic_distribution",
  349. ) # add the inverse function used to trigger the creation/update of the analytic lines accordingly (field originally defined in the analytic mixin)
  350. # === Early Pay fields === #
  351. discount_date = fields.Date(
  352. string='Discount Date',
  353. store=True,
  354. help='Last date at which the discounted amount must be paid in order for the Early Payment Discount to be granted'
  355. )
  356. # Discounted amount to pay when the early payment discount is applied
  357. discount_amount_currency = fields.Monetary(
  358. string='Discount amount in Currency',
  359. store=True,
  360. currency_field='currency_id',
  361. )
  362. # Discounted balance when the early payment discount is applied
  363. discount_balance = fields.Monetary(
  364. string='Discount Balance',
  365. store=True,
  366. currency_field='company_currency_id',
  367. )
  368. discount_percentage = fields.Float(store=True,)
  369. # === Misc Information === #
  370. blocked = fields.Boolean(
  371. string='No Follow-up',
  372. default=False,
  373. help="You can check this box to mark this journal item as a litigation with the "
  374. "associated partner",
  375. )
  376. is_refund = fields.Boolean(compute='_compute_is_refund')
  377. _sql_constraints = [
  378. (
  379. "check_credit_debit",
  380. "CHECK(display_type IN ('line_section', 'line_note') OR credit * debit=0)",
  381. "Wrong credit or debit value in accounting entry !"
  382. ),
  383. (
  384. "check_amount_currency_balance_sign",
  385. """CHECK(
  386. display_type IN ('line_section', 'line_note')
  387. OR (
  388. (balance <= 0 AND amount_currency <= 0)
  389. OR
  390. (balance >= 0 AND amount_currency >= 0)
  391. )
  392. )""",
  393. "The amount expressed in the secondary currency must be positive when account is debited and negative when "
  394. "account is credited. If the currency is the same as the one from the company, this amount must strictly "
  395. "be equal to the balance."
  396. ),
  397. (
  398. "check_accountable_required_fields",
  399. "CHECK(display_type IN ('line_section', 'line_note') OR account_id IS NOT NULL)",
  400. "Missing required account on accountable line."
  401. ),
  402. (
  403. "check_non_accountable_fields_null",
  404. "CHECK(display_type NOT IN ('line_section', 'line_note') OR (amount_currency = 0 AND debit = 0 AND credit = 0 AND account_id IS NULL))",
  405. "Forbidden balance or account on non-accountable line"
  406. ),
  407. ]
  408. # -------------------------------------------------------------------------
  409. # COMPUTE METHODS
  410. # -------------------------------------------------------------------------
  411. @api.depends('move_id')
  412. def _compute_display_type(self):
  413. for line in self.filtered(lambda l: not l.display_type):
  414. # avoid cyclic dependencies with _compute_account_id
  415. account_set = self.env.cache.contains(line, line._fields['account_id'])
  416. tax_set = self.env.cache.contains(line, line._fields['tax_line_id'])
  417. line.display_type = (
  418. 'tax' if tax_set and line.tax_line_id else
  419. 'payment_term' if account_set and line.account_id.account_type in ['asset_receivable', 'liability_payable'] else
  420. 'product'
  421. ) if line.move_id.is_invoice() else 'product'
  422. # Do not depend on `move_id.partner_id`, the inverse is taking care of that
  423. def _compute_partner_id(self):
  424. for line in self:
  425. line.partner_id = line.move_id.partner_id.commercial_partner_id
  426. @api.depends('move_id.currency_id')
  427. def _compute_currency_id(self):
  428. for line in self:
  429. if line.display_type == 'cogs':
  430. line.currency_id = line.company_currency_id
  431. elif line.move_id.is_invoice(include_receipts=True):
  432. line.currency_id = line.move_id.currency_id
  433. else:
  434. line.currency_id = line.currency_id or line.company_id.currency_id
  435. @api.depends('product_id')
  436. def _compute_name(self):
  437. for line in self:
  438. if line.display_type == 'payment_term':
  439. if line.move_id.payment_reference:
  440. line.name = line.move_id.payment_reference
  441. elif not line.name:
  442. line.name = ''
  443. continue
  444. if not line.product_id or line.display_type in ('line_section', 'line_note'):
  445. continue
  446. if line.partner_id.lang:
  447. product = line.product_id.with_context(lang=line.partner_id.lang)
  448. else:
  449. product = line.product_id
  450. values = []
  451. if product.partner_ref:
  452. values.append(product.partner_ref)
  453. if line.journal_id.type == 'sale':
  454. if product.description_sale:
  455. values.append(product.description_sale)
  456. elif line.journal_id.type == 'purchase':
  457. if product.description_purchase:
  458. values.append(product.description_purchase)
  459. line.name = '\n'.join(values)
  460. def _compute_account_id(self):
  461. term_lines = self.filtered(lambda line: line.display_type == 'payment_term')
  462. if term_lines:
  463. moves = term_lines.move_id
  464. self.env.cr.execute("""
  465. WITH previous AS (
  466. SELECT DISTINCT ON (line.move_id)
  467. 'account.move' AS model,
  468. line.move_id AS id,
  469. NULL AS account_type,
  470. line.account_id AS account_id
  471. FROM account_move_line line
  472. WHERE line.move_id = ANY(%(move_ids)s)
  473. AND line.display_type = 'payment_term'
  474. AND line.id != ANY(%(current_ids)s)
  475. ),
  476. properties AS(
  477. SELECT DISTINCT ON (property.company_id, property.name, property.res_id)
  478. 'res.partner' AS model,
  479. SPLIT_PART(property.res_id, ',', 2)::integer AS id,
  480. CASE
  481. WHEN property.name = 'property_account_receivable_id' THEN 'asset_receivable'
  482. ELSE 'liability_payable'
  483. END AS account_type,
  484. SPLIT_PART(property.value_reference, ',', 2)::integer AS account_id
  485. FROM ir_property property
  486. JOIN res_company company ON property.company_id = company.id
  487. WHERE property.name IN ('property_account_receivable_id', 'property_account_payable_id')
  488. AND property.company_id = ANY(%(company_ids)s)
  489. AND property.res_id = ANY(%(partners)s)
  490. ORDER BY property.company_id, property.name, property.res_id, account_id
  491. ),
  492. default_properties AS(
  493. SELECT DISTINCT ON (property.company_id, property.name)
  494. 'res.partner' AS model,
  495. company.partner_id AS id,
  496. CASE
  497. WHEN property.name = 'property_account_receivable_id' THEN 'asset_receivable'
  498. ELSE 'liability_payable'
  499. END AS account_type,
  500. SPLIT_PART(property.value_reference, ',', 2)::integer AS account_id
  501. FROM ir_property property
  502. JOIN res_company company ON property.company_id = company.id
  503. WHERE property.name IN ('property_account_receivable_id', 'property_account_payable_id')
  504. AND property.company_id = ANY(%(company_ids)s)
  505. AND property.res_id IS NULL
  506. ORDER BY property.company_id, property.name, account_id
  507. ),
  508. fallback AS (
  509. SELECT DISTINCT ON (account.company_id, account.account_type)
  510. 'res.company' AS model,
  511. account.company_id AS id,
  512. account.account_type AS account_type,
  513. account.id AS account_id
  514. FROM account_account account
  515. WHERE account.company_id = ANY(%(company_ids)s)
  516. AND account.account_type IN ('asset_receivable', 'liability_payable')
  517. AND account.deprecated = 'f'
  518. )
  519. SELECT * FROM previous
  520. UNION ALL
  521. SELECT * FROM properties
  522. UNION ALL
  523. SELECT * FROM default_properties
  524. UNION ALL
  525. SELECT * FROM fallback
  526. """, {
  527. 'company_ids': moves.company_id.ids,
  528. 'move_ids': moves.ids,
  529. 'partners': [f'res.partner,{pid}' for pid in moves.commercial_partner_id.ids],
  530. 'current_ids': term_lines.ids
  531. })
  532. accounts = {
  533. (model, id, account_type): account_id
  534. for model, id, account_type, account_id in self.env.cr.fetchall()
  535. }
  536. for line in term_lines:
  537. account_type = 'asset_receivable' if line.move_id.is_sale_document(include_receipts=True) else 'liability_payable'
  538. move = line.move_id
  539. account_id = (
  540. accounts.get(('account.move', move.id, None))
  541. or accounts.get(('res.partner', move.commercial_partner_id.id, account_type))
  542. or accounts.get(('res.partner', move.company_id.partner_id.id, account_type))
  543. or accounts.get(('res.company', move.company_id.id, account_type))
  544. )
  545. if line.move_id.fiscal_position_id:
  546. account_id = self.move_id.fiscal_position_id.map_account(self.env['account.account'].browse(account_id))
  547. line.account_id = account_id
  548. product_lines = self.filtered(lambda line: line.display_type == 'product' and line.move_id.is_invoice(True))
  549. for line in product_lines:
  550. if line.product_id:
  551. fiscal_position = line.move_id.fiscal_position_id
  552. accounts = line.with_company(line.company_id).product_id\
  553. .product_tmpl_id.get_product_accounts(fiscal_pos=fiscal_position)
  554. if line.move_id.is_sale_document(include_receipts=True):
  555. line.account_id = accounts['income'] or line.account_id
  556. elif line.move_id.is_purchase_document(include_receipts=True):
  557. line.account_id = accounts['expense'] or line.account_id
  558. elif line.partner_id:
  559. line.account_id = self.env['account.account']._get_most_frequent_account_for_partner(
  560. company_id=line.company_id.id,
  561. partner_id=line.partner_id.id,
  562. move_type=line.move_id.move_type,
  563. )
  564. for line in self:
  565. if not line.account_id and line.display_type not in ('line_section', 'line_note'):
  566. previous_two_accounts = line.move_id.line_ids.filtered(
  567. lambda l: l.account_id and l.display_type == line.display_type
  568. )[-2:].account_id
  569. if len(previous_two_accounts) == 1 and len(line.move_id.line_ids) > 2:
  570. line.account_id = previous_two_accounts
  571. else:
  572. line.account_id = line.move_id.journal_id.default_account_id
  573. @api.depends('move_id')
  574. def _compute_balance(self):
  575. for line in self:
  576. if line.display_type in ('line_section', 'line_note'):
  577. line.balance = False
  578. elif not line.move_id.is_invoice(include_receipts=True):
  579. # Only act as a default value when none of balance/debit/credit is specified
  580. # balance is always the written field because of `_sanitize_vals`
  581. line.balance = -sum((line.move_id.line_ids - line).mapped('balance'))
  582. else:
  583. line.balance = 0
  584. @api.depends('balance', 'move_id.is_storno')
  585. def _compute_debit_credit(self):
  586. for line in self:
  587. if not line.is_storno:
  588. line.debit = line.balance if line.balance > 0.0 else 0.0
  589. line.credit = -line.balance if line.balance < 0.0 else 0.0
  590. else:
  591. line.debit = line.balance if line.balance < 0.0 else 0.0
  592. line.credit = -line.balance if line.balance > 0.0 else 0.0
  593. @api.depends('currency_id', 'company_id', 'move_id.date')
  594. def _compute_currency_rate(self):
  595. @lru_cache()
  596. def get_rate(from_currency, to_currency, company, date):
  597. return self.env['res.currency']._get_conversion_rate(
  598. from_currency=from_currency,
  599. to_currency=to_currency,
  600. company=company,
  601. date=date,
  602. )
  603. for line in self:
  604. if line.currency_id:
  605. line.currency_rate = get_rate(
  606. from_currency=line.company_currency_id,
  607. to_currency=line.currency_id,
  608. company=line.company_id,
  609. date=line.move_id.invoice_date or line.move_id.date or fields.Date.context_today(line),
  610. )
  611. else:
  612. line.currency_rate = 1
  613. @api.depends('currency_id', 'company_currency_id')
  614. def _compute_same_currency(self):
  615. for record in self:
  616. record.is_same_currency = record.currency_id == record.company_currency_id
  617. @api.depends('currency_rate', 'balance')
  618. def _compute_amount_currency(self):
  619. for line in self:
  620. if line.amount_currency is False:
  621. line.amount_currency = line.currency_id.round(line.balance * line.currency_rate)
  622. if line.currency_id == line.company_id.currency_id:
  623. line.amount_currency = line.balance
  624. @api.depends('full_reconcile_id.name', 'matched_debit_ids', 'matched_credit_ids')
  625. def _compute_matching_number(self):
  626. for record in self:
  627. if record.full_reconcile_id:
  628. record.matching_number = record.full_reconcile_id.name
  629. elif record.matched_debit_ids or record.matched_credit_ids:
  630. record.matching_number = 'P'
  631. else:
  632. record.matching_number = None
  633. @api.depends_context('order_cumulated_balance', 'domain_cumulated_balance')
  634. def _compute_cumulated_balance(self):
  635. if not self.env.context.get('order_cumulated_balance'):
  636. # We do not come from search_read, so we are not in a list view, so it doesn't make any sense to compute the cumulated balance
  637. self.cumulated_balance = 0
  638. return
  639. # get the where clause
  640. query = self._where_calc(list(self.env.context.get('domain_cumulated_balance') or []))
  641. order_string = ", ".join(self._generate_order_by_inner(self._table, self.env.context.get('order_cumulated_balance'), query, reverse_direction=True))
  642. from_clause, where_clause, where_clause_params = query.get_sql()
  643. sql = """
  644. SELECT account_move_line.id, SUM(account_move_line.balance) OVER (
  645. ORDER BY %(order_by)s
  646. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  647. )
  648. FROM %(from)s
  649. WHERE %(where)s
  650. """ % {'from': from_clause, 'where': where_clause or 'TRUE', 'order_by': order_string}
  651. self.env.cr.execute(sql, where_clause_params)
  652. result = {r[0]: r[1] for r in self.env.cr.fetchall()}
  653. for record in self:
  654. record.cumulated_balance = result[record.id]
  655. @api.depends('debit', 'credit', 'amount_currency', 'account_id', 'currency_id', 'company_id',
  656. 'matched_debit_ids', 'matched_credit_ids')
  657. def _compute_amount_residual(self):
  658. """ Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
  659. This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
  660. for unreconciled lines, and something in-between for partially reconciled lines.
  661. """
  662. need_residual_lines = self.filtered(lambda x: x.account_id.reconcile or x.account_id.account_type in ('asset_cash', 'liability_credit_card'))
  663. # Run the residual amount computation on all lines stored in the db. By
  664. # using _origin, new records (with a NewId) are excluded and the
  665. # computation works automagically for virtual onchange records as well.
  666. stored_lines = need_residual_lines._origin
  667. if stored_lines:
  668. self.env['account.partial.reconcile'].flush_model()
  669. self.env['res.currency'].flush_model(['decimal_places'])
  670. aml_ids = tuple(stored_lines.ids)
  671. self._cr.execute('''
  672. SELECT
  673. part.debit_move_id AS line_id,
  674. 'debit' AS flag,
  675. COALESCE(SUM(part.amount), 0.0) AS amount,
  676. ROUND(SUM(part.debit_amount_currency), curr.decimal_places) AS amount_currency
  677. FROM account_partial_reconcile part
  678. JOIN res_currency curr ON curr.id = part.debit_currency_id
  679. WHERE part.debit_move_id IN %s
  680. GROUP BY part.debit_move_id, curr.decimal_places
  681. UNION ALL
  682. SELECT
  683. part.credit_move_id AS line_id,
  684. 'credit' AS flag,
  685. COALESCE(SUM(part.amount), 0.0) AS amount,
  686. ROUND(SUM(part.credit_amount_currency), curr.decimal_places) AS amount_currency
  687. FROM account_partial_reconcile part
  688. JOIN res_currency curr ON curr.id = part.credit_currency_id
  689. WHERE part.credit_move_id IN %s
  690. GROUP BY part.credit_move_id, curr.decimal_places
  691. ''', [aml_ids, aml_ids])
  692. amounts_map = {
  693. (line_id, flag): (amount, amount_currency)
  694. for line_id, flag, amount, amount_currency in self.env.cr.fetchall()
  695. }
  696. else:
  697. amounts_map = {}
  698. # Lines that can't be reconciled with anything since the account doesn't allow that.
  699. for line in self - need_residual_lines:
  700. line.amount_residual = 0.0
  701. line.amount_residual_currency = 0.0
  702. line.reconciled = False
  703. for line in need_residual_lines:
  704. # Since this part could be call on 'new' records, 'company_currency_id'/'currency_id' could be not set.
  705. comp_curr = line.company_currency_id or self.env.company.currency_id
  706. foreign_curr = line.currency_id or comp_curr
  707. # Retrieve the amounts in both foreign/company currencies. If the record is 'new', the amounts_map is empty.
  708. debit_amount, debit_amount_currency = amounts_map.get((line._origin.id, 'debit'), (0.0, 0.0))
  709. credit_amount, credit_amount_currency = amounts_map.get((line._origin.id, 'credit'), (0.0, 0.0))
  710. # Subtract the values from the account.partial.reconcile to compute the residual amounts.
  711. line.amount_residual = comp_curr.round(line.balance - debit_amount + credit_amount)
  712. line.amount_residual_currency = foreign_curr.round(line.amount_currency - debit_amount_currency + credit_amount_currency)
  713. line.reconciled = (
  714. comp_curr.is_zero(line.amount_residual)
  715. and foreign_curr.is_zero(line.amount_residual_currency)
  716. )
  717. @api.depends('move_id.move_type', 'tax_ids', 'tax_repartition_line_id', 'debit', 'credit', 'tax_tag_ids', 'is_refund')
  718. def _compute_tax_tag_invert(self):
  719. for record in self:
  720. if not record.tax_repartition_line_id and not record.tax_ids:
  721. # Invoices imported from other softwares might only have kept the tags, not the taxes.
  722. record.tax_tag_invert = record.tax_tag_ids and record.move_id.is_inbound()
  723. elif record.move_id.move_type == 'entry':
  724. # For misc operations, cash basis entries and write-offs from the bank reconciliation widget
  725. tax = record.tax_repartition_line_id.tax_id or record.tax_ids[:1]
  726. is_refund = record.is_refund
  727. tax_type = tax.type_tax_use
  728. record.tax_tag_invert = (tax_type == 'purchase' and is_refund) or (tax_type == 'sale' and not is_refund)
  729. else:
  730. # For invoices with taxes
  731. record.tax_tag_invert = record.move_id.is_inbound()
  732. @api.depends('tax_tag_ids', 'debit', 'credit', 'journal_id', 'tax_tag_invert')
  733. def _compute_tax_audit(self):
  734. separator = ' '
  735. for record in self:
  736. currency = record.company_id.currency_id
  737. audit_str = ''
  738. for tag in record.tax_tag_ids:
  739. tag_amount = (record.tax_tag_invert and -1 or 1) * (tag.tax_negate and -1 or 1) * record.balance
  740. if tag.applicability == 'taxes' and tag.name[0] in {'+', '-'}:
  741. # Then, the tag comes from a report expression, and hence has a + or - sign (also in its name)
  742. tag_name = tag.name[1:]
  743. else:
  744. # Then, it's a financial tag (sign is always +, and never shown in tag name)
  745. tag_name = tag.name
  746. audit_str += separator if audit_str else ''
  747. audit_str += tag_name + ': ' + formatLang(self.env, tag_amount, currency_obj=currency)
  748. record.tax_audit = audit_str
  749. @api.depends('product_id')
  750. def _compute_product_uom_id(self):
  751. for line in self:
  752. line.product_uom_id = line.product_id.uom_id
  753. @api.depends('display_type')
  754. def _compute_quantity(self):
  755. for line in self:
  756. line.quantity = 1 if line.display_type == 'product' else False
  757. @api.depends('display_type')
  758. def _compute_sequence(self):
  759. seq_map = {
  760. 'tax': 10000,
  761. 'rounding': 11000,
  762. 'payment_term': 12000,
  763. }
  764. for line in self:
  765. line.sequence = seq_map.get(line.display_type, 100)
  766. @api.depends('quantity', 'discount', 'price_unit', 'tax_ids', 'currency_id')
  767. def _compute_totals(self):
  768. for line in self:
  769. if line.display_type != 'product':
  770. line.price_total = line.price_subtotal = False
  771. # Compute 'price_subtotal'.
  772. line_discount_price_unit = line.price_unit * (1 - (line.discount / 100.0))
  773. subtotal = line.quantity * line_discount_price_unit
  774. # Compute 'price_total'.
  775. if line.tax_ids:
  776. taxes_res = line.tax_ids.compute_all(
  777. line_discount_price_unit,
  778. quantity=line.quantity,
  779. currency=line.currency_id,
  780. product=line.product_id,
  781. partner=line.partner_id,
  782. is_refund=line.is_refund,
  783. )
  784. line.price_subtotal = taxes_res['total_excluded']
  785. line.price_total = taxes_res['total_included']
  786. else:
  787. line.price_total = line.price_subtotal = subtotal
  788. @api.depends('product_id', 'product_uom_id')
  789. def _compute_price_unit(self):
  790. for line in self:
  791. if not line.product_id or line.display_type in ('line_section', 'line_note'):
  792. continue
  793. if line.move_id.is_sale_document(include_receipts=True):
  794. document_type = 'sale'
  795. elif line.move_id.is_purchase_document(include_receipts=True):
  796. document_type = 'purchase'
  797. else:
  798. document_type = 'other'
  799. line.price_unit = line.product_id._get_tax_included_unit_price(
  800. line.move_id.company_id,
  801. line.move_id.currency_id,
  802. line.move_id.date,
  803. document_type,
  804. fiscal_position=line.move_id.fiscal_position_id,
  805. product_uom=line.product_uom_id,
  806. )
  807. @api.depends('product_id', 'product_uom_id')
  808. def _compute_tax_ids(self):
  809. for line in self:
  810. if line.display_type in ('line_section', 'line_note', 'payment_term'):
  811. continue
  812. # /!\ Don't remove existing taxes if there is no explicit taxes set on the account.
  813. if line.product_id or line.account_id.tax_ids or not line.tax_ids:
  814. line.tax_ids = line._get_computed_taxes()
  815. def _get_computed_taxes(self):
  816. self.ensure_one()
  817. if self.move_id.is_sale_document(include_receipts=True):
  818. # Out invoice.
  819. if self.product_id.taxes_id:
  820. tax_ids = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == self.move_id.company_id)
  821. else:
  822. tax_ids = self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'sale')
  823. if not tax_ids and self.display_type == 'product':
  824. tax_ids = self.move_id.company_id.account_sale_tax_id
  825. elif self.move_id.is_purchase_document(include_receipts=True):
  826. # In invoice.
  827. if self.product_id.supplier_taxes_id:
  828. tax_ids = self.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == self.move_id.company_id)
  829. else:
  830. tax_ids = self.account_id.tax_ids.filtered(lambda tax: tax.type_tax_use == 'purchase')
  831. if not tax_ids and self.display_type == 'product':
  832. tax_ids = self.move_id.company_id.account_purchase_tax_id
  833. else:
  834. # Miscellaneous operation.
  835. tax_ids = self.account_id.tax_ids
  836. if self.company_id and tax_ids:
  837. tax_ids = tax_ids.filtered(lambda tax: tax.company_id == self.company_id)
  838. if tax_ids and self.move_id.fiscal_position_id:
  839. tax_ids = self.move_id.fiscal_position_id.map_tax(tax_ids)
  840. return tax_ids
  841. @api.depends('tax_ids', 'currency_id', 'partner_id', 'account_id', 'group_tax_id', 'analytic_distribution')
  842. def _compute_tax_key(self):
  843. for line in self:
  844. if line.tax_repartition_line_id:
  845. line.tax_key = frozendict({
  846. 'tax_repartition_line_id': line.tax_repartition_line_id.id,
  847. 'group_tax_id': line.group_tax_id.id,
  848. 'account_id': line.account_id.id,
  849. 'currency_id': line.currency_id.id,
  850. 'analytic_distribution': line.analytic_distribution,
  851. 'tax_ids': [(6, 0, line.tax_ids.ids)],
  852. 'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)],
  853. 'partner_id': line.partner_id.id,
  854. 'move_id': line.move_id.id,
  855. 'display_type': 'epd' if line.name and _('(Discount)') in line.name else line.display_type,
  856. })
  857. else:
  858. line.tax_key = frozendict({'id': line.id})
  859. @api.depends('tax_ids', 'currency_id', 'partner_id', 'analytic_distribution', 'balance', 'partner_id', 'move_id.partner_id', 'price_unit', 'quantity')
  860. def _compute_all_tax(self):
  861. for line in self:
  862. sign = line.move_id.direction_sign
  863. if line.display_type == 'tax':
  864. line.compute_all_tax = {}
  865. line.compute_all_tax_dirty = False
  866. continue
  867. if line.display_type == 'product' and line.move_id.is_invoice(True):
  868. amount_currency = sign * line.price_unit * (1 - line.discount / 100)
  869. handle_price_include = True
  870. quantity = line.quantity
  871. else:
  872. amount_currency = line.amount_currency
  873. handle_price_include = False
  874. quantity = 1
  875. compute_all_currency = line.tax_ids.compute_all(
  876. amount_currency,
  877. currency=line.currency_id,
  878. quantity=quantity,
  879. product=line.product_id,
  880. partner=line.move_id.partner_id or line.partner_id,
  881. is_refund=line.is_refund,
  882. handle_price_include=handle_price_include,
  883. include_caba_tags=line.move_id.always_tax_exigible,
  884. fixed_multiplicator=sign,
  885. )
  886. rate = line.amount_currency / line.balance if line.balance else 1
  887. line.compute_all_tax_dirty = True
  888. line.compute_all_tax = {
  889. frozendict({
  890. 'tax_repartition_line_id': tax['tax_repartition_line_id'],
  891. 'group_tax_id': tax['group'] and tax['group'].id or False,
  892. 'account_id': tax['account_id'] or line.account_id.id,
  893. 'currency_id': line.currency_id.id,
  894. 'analytic_distribution': (tax['analytic'] or not tax['use_in_tax_closing']) and line.analytic_distribution,
  895. 'tax_ids': [(6, 0, tax['tax_ids'])],
  896. 'tax_tag_ids': [(6, 0, tax['tag_ids'])],
  897. 'partner_id': line.move_id.partner_id.id or line.partner_id.id,
  898. 'move_id': line.move_id.id,
  899. 'display_type': line.display_type,
  900. }): {
  901. 'name': tax['name'] + (' ' + _('(Discount)') if line.display_type == 'epd' else ''),
  902. 'balance': tax['amount'] / rate,
  903. 'amount_currency': tax['amount'],
  904. 'tax_base_amount': tax['base'] / rate * (-1 if line.tax_tag_invert else 1),
  905. }
  906. for tax in compute_all_currency['taxes']
  907. if tax['amount']
  908. }
  909. if not line.tax_repartition_line_id:
  910. line.compute_all_tax[frozendict({'id': line.id})] = {
  911. 'tax_tag_ids': [(6, 0, compute_all_currency['base_tags'])],
  912. }
  913. @api.depends('tax_ids', 'account_id', 'company_id')
  914. def _compute_epd_key(self):
  915. for line in self:
  916. if line.display_type == 'epd' and line.company_id.early_pay_discount_computation == 'mixed':
  917. line.epd_key = frozendict({
  918. 'account_id': line.account_id.id,
  919. 'analytic_distribution': line.analytic_distribution,
  920. 'tax_ids': [Command.set(line.tax_ids.ids)],
  921. 'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)],
  922. 'move_id': line.move_id.id,
  923. })
  924. else:
  925. line.epd_key = False
  926. @api.depends('move_id.needed_terms', 'account_id', 'analytic_distribution', 'tax_ids', 'tax_tag_ids', 'company_id')
  927. def _compute_epd_needed(self):
  928. for line in self:
  929. needed_terms = line.move_id.needed_terms
  930. line.epd_dirty = True
  931. line.epd_needed = False
  932. if line.display_type != 'product' or not line.tax_ids.ids or line.company_id.early_pay_discount_computation != 'mixed':
  933. continue
  934. amount_total = abs(sum(x['amount_currency'] for x in needed_terms.values()))
  935. percentages_to_apply = []
  936. names = []
  937. for term in needed_terms.values():
  938. if term.get('discount_percentage'):
  939. percentages_to_apply.append({
  940. 'discount_percentage': term['discount_percentage'],
  941. 'term_percentage': abs(term['amount_currency'] / amount_total) if amount_total else 0
  942. })
  943. names.append(f"{term['discount_percentage']}%")
  944. discount_percentage_name = ', '.join(names)
  945. epd_needed = {}
  946. for percentages in percentages_to_apply:
  947. percentage = percentages['discount_percentage'] / 100
  948. line_percentage = percentages['term_percentage']
  949. taxes = line.tax_ids.filtered(lambda t: t.amount_type != 'fixed')
  950. epd_needed_vals = epd_needed.setdefault(
  951. frozendict({
  952. 'move_id': line.move_id.id,
  953. 'account_id': line.account_id.id,
  954. 'analytic_distribution': line.analytic_distribution,
  955. 'tax_ids': [Command.set(taxes.ids)],
  956. 'tax_tag_ids': line.compute_all_tax[frozendict({'id': line.id})]['tax_tag_ids'],
  957. 'display_type': 'epd',
  958. }),
  959. {
  960. 'name': _("Early Payment Discount (%s)", discount_percentage_name),
  961. 'amount_currency': 0.0,
  962. 'balance': 0.0,
  963. 'price_subtotal': 0.0,
  964. },
  965. )
  966. epd_needed_vals['amount_currency'] -= line.currency_id.round(line.amount_currency * percentage * line_percentage)
  967. epd_needed_vals['balance'] -= line.currency_id.round(line.balance * percentage * line_percentage)
  968. epd_needed_vals['price_subtotal'] -= line.currency_id.round(line.price_subtotal * percentage * line_percentage)
  969. epd_needed_vals = epd_needed.setdefault(
  970. frozendict({
  971. 'move_id': line.move_id.id,
  972. 'account_id': line.account_id.id,
  973. 'display_type': 'epd',
  974. }),
  975. {
  976. 'name': _("Early Payment Discount (%s)", discount_percentage_name),
  977. 'amount_currency': 0.0,
  978. 'balance': 0.0,
  979. 'price_subtotal': 0.0,
  980. 'tax_ids': [Command.clear()],
  981. },
  982. )
  983. epd_needed_vals['amount_currency'] += line.currency_id.round(line.amount_currency * percentage * line_percentage)
  984. epd_needed_vals['balance'] += line.currency_id.round(line.balance * percentage * line_percentage)
  985. epd_needed_vals['price_subtotal'] += line.currency_id.round(line.price_subtotal * percentage * line_percentage)
  986. line.epd_needed = {k: frozendict(v) for k, v in epd_needed.items()}
  987. @api.depends('move_id.move_type', 'balance', 'tax_repartition_line_id', 'tax_ids')
  988. def _compute_is_refund(self):
  989. for line in self:
  990. is_refund = False
  991. if line.move_id.move_type in ('out_refund', 'in_refund'):
  992. is_refund = True
  993. elif line.move_id.move_type == 'entry':
  994. if line.tax_repartition_line_id:
  995. is_refund = bool(line.tax_repartition_line_id.refund_tax_id)
  996. else:
  997. tax_type = line.tax_ids[:1].type_tax_use
  998. if tax_type == 'sale' and line.credit == 0:
  999. is_refund = True
  1000. elif tax_type == 'purchase' and line.debit == 0:
  1001. is_refund = True
  1002. if line.tax_ids and line.move_id.reversed_entry_id:
  1003. is_refund = not is_refund
  1004. line.is_refund = is_refund
  1005. @api.depends('date_maturity')
  1006. def _compute_term_key(self):
  1007. for line in self:
  1008. if line.display_type == 'payment_term':
  1009. line.term_key = frozendict({
  1010. 'move_id': line.move_id.id,
  1011. 'date_maturity': fields.Date.to_date(line.date_maturity),
  1012. 'discount_date': line.discount_date,
  1013. 'discount_percentage': line.discount_percentage
  1014. })
  1015. else:
  1016. line.term_key = False
  1017. @api.depends('account_id', 'partner_id', 'product_id')
  1018. def _compute_analytic_distribution(self):
  1019. for line in self:
  1020. if line.display_type == 'product' or not line.move_id.is_invoice(include_receipts=True):
  1021. distribution = self.env['account.analytic.distribution.model']._get_distribution({
  1022. "product_id": line.product_id.id,
  1023. "product_categ_id": line.product_id.categ_id.id,
  1024. "partner_id": line.partner_id.id,
  1025. "partner_category_id": line.partner_id.category_id.ids,
  1026. "account_prefix": line.account_id.code,
  1027. "company_id": line.company_id.id,
  1028. })
  1029. line.analytic_distribution = distribution or line.analytic_distribution
  1030. # -------------------------------------------------------------------------
  1031. # INVERSE METHODS
  1032. # -------------------------------------------------------------------------
  1033. @api.onchange('partner_id')
  1034. def _inverse_partner_id(self):
  1035. self._conditional_add_to_compute('account_id', lambda line: (
  1036. line.display_type == 'payment_term' # recompute based on settings
  1037. ))
  1038. @api.onchange('product_id')
  1039. def _inverse_product_id(self):
  1040. self._conditional_add_to_compute('account_id', lambda line: (
  1041. line.display_type == 'product' and line.move_id.is_invoice(True)
  1042. ))
  1043. @api.onchange('amount_currency', 'currency_id')
  1044. def _inverse_amount_currency(self):
  1045. for line in self:
  1046. if line.currency_id == line.company_id.currency_id and line.balance != line.amount_currency:
  1047. line.balance = line.amount_currency
  1048. elif (
  1049. line.currency_id != line.company_id.currency_id
  1050. and not line.move_id.is_invoice(True)
  1051. and not self.env.is_protected(self._fields['balance'], line)
  1052. ):
  1053. line.balance = line.company_id.currency_id.round(line.amount_currency / line.currency_rate)
  1054. @api.onchange('debit')
  1055. def _inverse_debit(self):
  1056. for line in self:
  1057. if line.debit:
  1058. line.credit = 0
  1059. line.balance = line.debit - line.credit
  1060. @api.onchange('credit')
  1061. def _inverse_credit(self):
  1062. for line in self:
  1063. if line.credit:
  1064. line.debit = 0
  1065. line.balance = line.debit - line.credit
  1066. @api.onchange('analytic_distribution')
  1067. def _inverse_analytic_distribution(self):
  1068. """ Unlink and recreate analytic_lines when modifying the distribution."""
  1069. lines_to_modify = self.env['account.move.line'].browse([
  1070. line.id for line in self if line.parent_state == "posted"
  1071. ])
  1072. lines_to_modify.analytic_line_ids.unlink()
  1073. lines_to_modify._create_analytic_lines()
  1074. @api.onchange('account_id')
  1075. def _inverse_account_id(self):
  1076. self._inverse_analytic_distribution()
  1077. self._conditional_add_to_compute('tax_ids', lambda line: (
  1078. line.account_id.tax_ids
  1079. and not line.product_id.taxes_id.filtered(lambda tax: tax.company_id == line.company_id)
  1080. ))
  1081. # -------------------------------------------------------------------------
  1082. # CONSTRAINT METHODS
  1083. # -------------------------------------------------------------------------
  1084. def _check_constrains_account_id_journal_id(self):
  1085. # Avoid using api.constrains here as in case of a write on
  1086. # account move and account move line in the same operation, the check would be done
  1087. # before all write are complete, causing a false positive
  1088. self.flush_recordset()
  1089. for line in self.filtered(lambda x: x.display_type not in ('line_section', 'line_note')):
  1090. account = line.account_id
  1091. journal = line.move_id.journal_id
  1092. if account.deprecated:
  1093. raise UserError(_('The account %s (%s) is deprecated.') % (account.name, account.code))
  1094. account_currency = account.currency_id
  1095. if account_currency and account_currency != line.company_currency_id and account_currency != line.currency_id:
  1096. raise UserError(_('The account selected on your journal entry forces to provide a secondary currency. You should remove the secondary currency on the account.'))
  1097. if account.allowed_journal_ids and journal not in account.allowed_journal_ids:
  1098. raise UserError(_('You cannot use this account (%s) in this journal, check the field \'Allowed Journals\' on the related account.', account.display_name))
  1099. if account in (journal.default_account_id, journal.suspense_account_id):
  1100. continue
  1101. is_account_control_ok = not journal.account_control_ids or account in journal.account_control_ids
  1102. if not is_account_control_ok:
  1103. raise UserError(_("You cannot use this account (%s) in this journal, check the section 'Control-Access' under "
  1104. "tab 'Advanced Settings' on the related journal.", account.display_name))
  1105. @api.constrains('account_id', 'tax_ids', 'tax_line_id', 'reconciled')
  1106. def _check_off_balance(self):
  1107. for line in self:
  1108. if line.account_id.internal_group == 'off_balance':
  1109. if any(a.internal_group != line.account_id.internal_group for a in line.move_id.line_ids.account_id):
  1110. raise UserError(_('If you want to use "Off-Balance Sheet" accounts, all the accounts of the journal entry must be of this type'))
  1111. if line.tax_ids or line.tax_line_id:
  1112. raise UserError(_('You cannot use taxes on lines with an Off-Balance account'))
  1113. if line.reconciled:
  1114. raise UserError(_('Lines from "Off-Balance Sheet" accounts cannot be reconciled'))
  1115. @api.constrains('account_id', 'display_type')
  1116. def _check_payable_receivable(self):
  1117. for line in self:
  1118. account_type = line.account_id.account_type
  1119. if line.move_id.is_sale_document(include_receipts=True):
  1120. if (line.display_type == 'payment_term') ^ (account_type == 'asset_receivable'):
  1121. raise UserError(_("Any journal item on a receivable account must have a due date and vice versa."))
  1122. if line.move_id.is_purchase_document(include_receipts=True):
  1123. if (line.display_type == 'payment_term') ^ (account_type == 'liability_payable'):
  1124. raise UserError(_("Any journal item on a payable account must have a due date and vice versa."))
  1125. @api.constrains('product_uom_id')
  1126. def _check_product_uom_category_id(self):
  1127. for line in self:
  1128. if line.product_uom_id and line.product_id and line.product_uom_id.category_id != line.product_id.product_tmpl_id.uom_id.category_id:
  1129. raise UserError(_(
  1130. "The Unit of Measure (UoM) '%s' you have selected for product '%s', "
  1131. "is incompatible with its category : %s.",
  1132. line.product_uom_id.name,
  1133. line.product_id.name,
  1134. line.product_id.product_tmpl_id.uom_id.category_id.name
  1135. ))
  1136. def _affect_tax_report(self):
  1137. self.ensure_one()
  1138. return self.tax_ids or self.tax_line_id or self.tax_tag_ids.filtered(lambda x: x.applicability == "taxes")
  1139. def _check_tax_lock_date(self):
  1140. for line in self.filtered(lambda l: l.move_id.state == 'posted'):
  1141. move = line.move_id
  1142. if move.company_id.tax_lock_date and move.date <= move.company_id.tax_lock_date and line._affect_tax_report():
  1143. raise UserError(_("The operation is refused as it would impact an already issued tax statement. "
  1144. "Please change the journal entry date or the tax lock date set in the settings (%s) to proceed.")
  1145. % format_date(self.env, move.company_id.tax_lock_date))
  1146. def _check_reconciliation(self):
  1147. for line in self:
  1148. if line.matched_debit_ids or line.matched_credit_ids:
  1149. raise UserError(_("You cannot do this modification on a reconciled journal entry. "
  1150. "You can just change some non legal fields or you must unreconcile first.\n"
  1151. "Journal Entry (id): %s (%s)") % (line.move_id.name, line.move_id.id))
  1152. @api.constrains('tax_ids', 'tax_repartition_line_id')
  1153. def _check_caba_non_caba_shared_tags(self):
  1154. """ When mixing cash basis and non cash basis taxes, it is important
  1155. that those taxes don't share tags on the repartition creating
  1156. a single account.move.line.
  1157. Shared tags in this context cannot work, as the tags would need to
  1158. be present on both the invoice and cash basis move, leading to the same
  1159. base amount to be taken into account twice; which is wrong.This is
  1160. why we don't support that. A workaround may be provided by the use of
  1161. a group of taxes, whose children are type_tax_use=None, and only one
  1162. of them uses the common tag.
  1163. Note that taxes of the same exigibility are allowed to share tags.
  1164. """
  1165. def get_base_repartition(base_aml, taxes):
  1166. if not taxes:
  1167. return self.env['account.tax.repartition.line']
  1168. is_refund = base_aml.is_refund
  1169. repartition_field = is_refund and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids'
  1170. return taxes.mapped(repartition_field)
  1171. for aml in self:
  1172. caba_taxes = aml.tax_ids.filtered(lambda x: x.tax_exigibility == 'on_payment')
  1173. non_caba_taxes = aml.tax_ids - caba_taxes
  1174. caba_base_tags = get_base_repartition(aml, caba_taxes).filtered(lambda x: x.repartition_type == 'base').tag_ids
  1175. non_caba_base_tags = get_base_repartition(aml, non_caba_taxes).filtered(lambda x: x.repartition_type == 'base').tag_ids
  1176. common_tags = caba_base_tags & non_caba_base_tags
  1177. if not common_tags:
  1178. # When a tax is affecting another one with different tax exigibility, tags cannot be shared either.
  1179. tax_tags = aml.tax_repartition_line_id.tag_ids
  1180. comparison_tags = non_caba_base_tags if aml.tax_repartition_line_id.tax_id.tax_exigibility == 'on_payment' else caba_base_tags
  1181. common_tags = tax_tags & comparison_tags
  1182. if common_tags:
  1183. raise ValidationError(_("Taxes exigible on payment and on invoice cannot be mixed on the same journal item if they share some tag."))
  1184. # -------------------------------------------------------------------------
  1185. # CRUD/ORM
  1186. # -------------------------------------------------------------------------
  1187. def check_field_access_rights(self, operation, field_names):
  1188. result = super().check_field_access_rights(operation, field_names)
  1189. if not field_names:
  1190. weirdos = ['term_key', 'tax_key', 'compute_all_tax', 'epd_key', 'epd_needed']
  1191. result = [fname for fname in result if fname not in weirdos]
  1192. return result
  1193. def invalidate_model(self, fnames=None, flush=True):
  1194. # Invalidate cache of related moves
  1195. if fnames is None or 'move_id' in fnames:
  1196. field = self._fields['move_id']
  1197. lines = self.env.cache.get_records(self, field)
  1198. move_ids = {id_ for id_ in self.env.cache.get_values(lines, field) if id_}
  1199. if move_ids:
  1200. self.env['account.move'].browse(move_ids).invalidate_recordset()
  1201. return super().invalidate_model(fnames=fnames, flush=flush)
  1202. def invalidate_recordset(self, fnames=None, flush=True):
  1203. # Invalidate cache of related moves
  1204. if fnames is None or 'move_id' in fnames:
  1205. field = self._fields['move_id']
  1206. move_ids = {id_ for id_ in self.env.cache.get_values(self, field) if id_}
  1207. if move_ids:
  1208. self.env['account.move'].browse(move_ids).invalidate_recordset()
  1209. return super().invalidate_recordset(fnames=fnames, flush=flush)
  1210. @api.model
  1211. def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
  1212. def to_tuple(t):
  1213. return tuple(map(to_tuple, t)) if isinstance(t, (list, tuple)) else t
  1214. # Make an explicit order because we will need to reverse it
  1215. order = (order or self._order) + ', id'
  1216. # Add the domain and order by in order to compute the cumulated balance in _compute_cumulated_balance
  1217. contextualized = self.with_context(
  1218. domain_cumulated_balance=to_tuple(domain or []),
  1219. order_cumulated_balance=order,
  1220. )
  1221. return super(AccountMoveLine, contextualized).search_read(domain, fields, offset, limit, order)
  1222. def init(self):
  1223. """ change index on partner_id to a multi-column index on (partner_id, ref), the new index will behave in the
  1224. same way when we search on partner_id, with the addition of being optimal when having a query that will
  1225. search on partner_id and ref at the same time (which is the case when we open the bank reconciliation widget)
  1226. """
  1227. create_index(self._cr, 'account_move_line_partner_id_ref_idx', 'account_move_line', ["partner_id", "ref"])
  1228. create_index(self._cr, 'account_move_line_date_name_id_idx', 'account_move_line', ["date desc", "move_name desc", "id"])
  1229. super().init()
  1230. def default_get(self, fields_list):
  1231. defaults = super().default_get(fields_list)
  1232. quick_encode_suggestion = self.env.context.get('quick_encoding_vals')
  1233. if quick_encode_suggestion:
  1234. defaults['account_id'] = quick_encode_suggestion['account_id']
  1235. defaults['price_unit'] = quick_encode_suggestion['price_unit']
  1236. defaults['tax_ids'] = [Command.set(quick_encode_suggestion['tax_ids'])]
  1237. return defaults
  1238. def _sanitize_vals(self, vals):
  1239. if 'debit' in vals or 'credit' in vals:
  1240. vals = vals.copy()
  1241. if 'balance' in vals:
  1242. vals.pop('debit', None)
  1243. vals.pop('credit', None)
  1244. else:
  1245. vals['balance'] = vals.pop('debit', 0) - vals.pop('credit', 0)
  1246. return vals
  1247. def _prepare_create_values(self, vals_list):
  1248. result_vals_list = super()._prepare_create_values(vals_list)
  1249. for init_vals, res_vals in zip(vals_list, result_vals_list):
  1250. # Allow computing the balance based on the amount_currency if it wasn't specified in the create vals.
  1251. if (
  1252. 'amount_currency' in init_vals
  1253. and 'balance' not in init_vals
  1254. and 'debit' not in init_vals
  1255. and 'credit' not in init_vals
  1256. ):
  1257. res_vals.pop('balance', 0)
  1258. res_vals.pop('debit', 0)
  1259. res_vals.pop('credit', 0)
  1260. return result_vals_list
  1261. @contextmanager
  1262. def _sync_invoice(self, container):
  1263. if container['records'].env.context.get('skip_invoice_line_sync'):
  1264. yield
  1265. return # avoid infinite recursion
  1266. def existing():
  1267. return {
  1268. line: {
  1269. 'amount_currency': line.currency_id.round(line.amount_currency),
  1270. 'balance': line.company_id.currency_id.round(line.balance),
  1271. 'currency_rate': line.currency_rate,
  1272. 'price_subtotal': line.currency_id.round(line.price_subtotal),
  1273. 'move_type': line.move_id.move_type,
  1274. } for line in container['records'].with_context(
  1275. skip_invoice_line_sync=True,
  1276. ).filtered(lambda l: l.move_id.is_invoice(True))
  1277. }
  1278. def changed(fname):
  1279. return line not in before or before[line][fname] != after[line][fname]
  1280. before = existing()
  1281. yield
  1282. after = existing()
  1283. for line in after:
  1284. if (
  1285. line.display_type == 'product'
  1286. and (not changed('amount_currency') or line not in before)
  1287. ):
  1288. amount_currency = line.move_id.direction_sign * line.currency_id.round(line.price_subtotal)
  1289. if line.amount_currency != amount_currency or line not in before:
  1290. line.amount_currency = amount_currency
  1291. if line.currency_id == line.company_id.currency_id:
  1292. line.balance = amount_currency
  1293. after = existing()
  1294. for line in after:
  1295. if (
  1296. (changed('amount_currency') or changed('currency_rate') or changed('move_type'))
  1297. and (not changed('balance') or (line not in before and not line.balance))
  1298. ):
  1299. balance = line.company_id.currency_id.round(line.amount_currency / line.currency_rate)
  1300. line.balance = balance
  1301. # Since this method is called during the sync, inside of `create`/`write`, these fields
  1302. # already have been computed and marked as so. But this method should re-trigger it since
  1303. # it changes the dependencies.
  1304. self.env.add_to_compute(self._fields['debit'], container['records'])
  1305. self.env.add_to_compute(self._fields['credit'], container['records'])
  1306. @api.model_create_multi
  1307. def create(self, vals_list):
  1308. moves = self.env['account.move'].browse({vals['move_id'] for vals in vals_list})
  1309. container = {'records': self}
  1310. move_container = {'records': moves}
  1311. with moves._check_balanced(move_container),\
  1312. moves._sync_dynamic_lines(move_container),\
  1313. self._sync_invoice(container):
  1314. lines = super().create([self._sanitize_vals(vals) for vals in vals_list])
  1315. container['records'] = lines
  1316. for line in lines:
  1317. if line.move_id.state == 'posted':
  1318. line._check_tax_lock_date()
  1319. lines.move_id._synchronize_business_models(['line_ids'])
  1320. lines._check_constrains_account_id_journal_id()
  1321. return lines
  1322. def write(self, vals):
  1323. if not vals:
  1324. return True
  1325. protected_fields = self._get_lock_date_protected_fields()
  1326. account_to_write = self.env['account.account'].browse(vals['account_id']) if 'account_id' in vals else None
  1327. # Check writing a deprecated account.
  1328. if account_to_write and account_to_write.deprecated:
  1329. raise UserError(_('You cannot use a deprecated account.'))
  1330. inalterable_fields = set(self._get_integrity_hash_fields()).union({'inalterable_hash', 'secure_sequence_number'})
  1331. hashed_moves = self.move_id.filtered('inalterable_hash')
  1332. violated_fields = set(vals) & inalterable_fields
  1333. if hashed_moves and violated_fields:
  1334. raise UserError(_(
  1335. "You cannot edit the following fields: %s.\n"
  1336. "The following entries are already hashed:\n%s",
  1337. ', '.join(f['string'] for f in self.fields_get(violated_fields).values()),
  1338. '\n'.join(hashed_moves.mapped('name')),
  1339. ))
  1340. line_to_write = self
  1341. vals = self._sanitize_vals(vals)
  1342. for line in self:
  1343. if not any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in vals):
  1344. line_to_write -= line
  1345. continue
  1346. if line.parent_state == 'posted':
  1347. if any(key in vals for key in ('tax_ids', 'tax_line_id')):
  1348. raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))
  1349. # Check the lock date.
  1350. if line.parent_state == 'posted' and any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in protected_fields['fiscal']):
  1351. line.move_id._check_fiscalyear_lock_date()
  1352. # Check the tax lock date.
  1353. if line.parent_state == 'posted' and any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in protected_fields['tax']):
  1354. line._check_tax_lock_date()
  1355. # Check the reconciliation.
  1356. if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in protected_fields['reconciliation']):
  1357. line._check_reconciliation()
  1358. move_container = {'records': self.move_id}
  1359. with self.move_id._check_balanced(move_container),\
  1360. self.move_id._sync_dynamic_lines(move_container),\
  1361. self._sync_invoice({'records': self}):
  1362. self = line_to_write
  1363. if not self:
  1364. return True
  1365. # Tracking stuff can be skipped for perfs using tracking_disable context key
  1366. if not self.env.context.get('tracking_disable', False):
  1367. # Get all tracked fields (without related fields because these fields must be manage on their own model)
  1368. tracking_fields = []
  1369. for value in vals:
  1370. field = self._fields[value]
  1371. if hasattr(field, 'related') and field.related:
  1372. continue # We don't want to track related field.
  1373. if hasattr(field, 'tracking') and field.tracking:
  1374. tracking_fields.append(value)
  1375. ref_fields = self.env['account.move.line'].fields_get(tracking_fields)
  1376. # Get initial values for each line
  1377. move_initial_values = {}
  1378. for line in self.filtered(lambda l: l.move_id.posted_before): # Only lines with posted once move.
  1379. for field in tracking_fields:
  1380. # Group initial values by move_id
  1381. if line.move_id.id not in move_initial_values:
  1382. move_initial_values[line.move_id.id] = {}
  1383. move_initial_values[line.move_id.id].update({field: line[field]})
  1384. result = super().write(vals)
  1385. self.move_id._synchronize_business_models(['line_ids'])
  1386. if any(field in vals for field in ['account_id', 'currency_id']):
  1387. self._check_constrains_account_id_journal_id()
  1388. if not self.env.context.get('tracking_disable', False):
  1389. # Log changes to move lines on each move
  1390. for move_id, modified_lines in move_initial_values.items():
  1391. for line in self.filtered(lambda l: l.move_id.id == move_id):
  1392. tracking_value_ids = line._mail_track(ref_fields, modified_lines)[1]
  1393. if tracking_value_ids:
  1394. msg = _(
  1395. "Journal Item %s updated",
  1396. line._get_html_link(title=f"#{line.id}")
  1397. )
  1398. line.move_id._message_log(
  1399. body=msg,
  1400. tracking_value_ids=tracking_value_ids
  1401. )
  1402. return result
  1403. def _valid_field_parameter(self, field, name):
  1404. # EXTENDS models
  1405. return name == 'tracking' or super()._valid_field_parameter(field, name)
  1406. @api.ondelete(at_uninstall=False)
  1407. def _unlink_except_posted(self):
  1408. # Prevent deleting lines on posted entries
  1409. if not self._context.get('force_delete') and any(m.state == 'posted' for m in self.move_id):
  1410. raise UserError(_('You cannot delete an item linked to a posted entry.'))
  1411. @api.ondelete(at_uninstall=False)
  1412. def _prevent_automatic_line_deletion(self):
  1413. if not self.env.context.get('dynamic_unlink'):
  1414. for line in self:
  1415. if line.display_type == 'tax' and line.move_id.line_ids.tax_ids:
  1416. raise ValidationError(_(
  1417. "You cannot delete a tax line as it would impact the tax report"
  1418. ))
  1419. elif line.display_type == 'payment_term':
  1420. raise ValidationError(_(
  1421. "You cannot delete a payable/receivable line as it would not be consistent "
  1422. "with the payment terms"
  1423. ))
  1424. def unlink(self):
  1425. if not self:
  1426. return
  1427. # Check the lines are not reconciled (partially or not).
  1428. self._check_reconciliation()
  1429. # Check the lock date. (Only relevant if the move is posted)
  1430. self.move_id.filtered(lambda m: m.state == 'posted')._check_fiscalyear_lock_date()
  1431. # Check the tax lock date.
  1432. self._check_tax_lock_date()
  1433. move_container = {'records': self.move_id}
  1434. with self.move_id._check_balanced(move_container),\
  1435. self.move_id._sync_dynamic_lines(move_container):
  1436. res = super().unlink()
  1437. return res
  1438. def name_get(self):
  1439. return [(line.id, " ".join(
  1440. element for element in (
  1441. line.move_id.name,
  1442. line.ref and f"({line.ref})",
  1443. line.name or line.product_id.display_name,
  1444. ) if element
  1445. )) for line in self]
  1446. def copy_data(self, default=None):
  1447. data_list = super().copy_data(default=default)
  1448. for line, values in zip(self, data_list):
  1449. # Don't copy the name of a payment term line.
  1450. if line.display_type == 'payment_term' and line.move_id.is_invoice(True):
  1451. del values['name']
  1452. # Don't copy restricted fields of notes
  1453. if line.display_type in ('line_section', 'line_note'):
  1454. del values['balance']
  1455. del values['account_id']
  1456. # Will be recomputed from the price_unit
  1457. if line.display_type == 'product' and line.move_id.is_invoice(True):
  1458. del values['balance']
  1459. if self._context.get('include_business_fields'):
  1460. line._copy_data_extend_business_fields(values)
  1461. return data_list
  1462. def _search_panel_domain_image(self, field_name, domain, set_count=False, limit=False):
  1463. if field_name != 'account_root_id' or set_count:
  1464. return super()._search_panel_domain_image(field_name, domain, set_count, limit)
  1465. # Override in order to not read the complete move line table and use the index instead
  1466. query = self._search(domain, limit=1)
  1467. # if domain is logically equivalent to false
  1468. if not isinstance(query, Query):
  1469. return {}
  1470. query.order = None
  1471. query.add_where('account.id = account_move_line.account_id')
  1472. query_str, query_param = query.select()
  1473. self.env.cr.execute(f"""
  1474. SELECT account.root_id
  1475. FROM account_account account,
  1476. LATERAL ({query_str}) line
  1477. WHERE account.company_id IN %s
  1478. """, query_param + [tuple(self.env.companies.ids)])
  1479. return {
  1480. root.id: {'id': root.id, 'display_name': root.display_name}
  1481. for root in self.env['account.root'].browse(id for [id] in self.env.cr.fetchall())
  1482. }
  1483. # -------------------------------------------------------------------------
  1484. # TRACKING METHODS
  1485. # -------------------------------------------------------------------------
  1486. def _mail_track(self, tracked_fields, initial):
  1487. changes, tracking_value_ids = super()._mail_track(tracked_fields, initial)
  1488. if len(changes) > len(tracking_value_ids):
  1489. for i, changed_field in enumerate(changes):
  1490. if tracked_fields[changed_field]['type'] in ['one2many', 'many2many']:
  1491. field = self.env['ir.model.fields']._get(self._name, changed_field)
  1492. vals = {
  1493. 'field': field.id,
  1494. 'field_desc': field.field_description,
  1495. 'field_type': field.ttype,
  1496. 'tracking_sequence': field.tracking,
  1497. 'old_value_char': ', '.join(initial[changed_field].mapped('name')),
  1498. 'new_value_char': ', '.join(self[changed_field].mapped('name')),
  1499. }
  1500. tracking_value_ids.insert(i, Command.create(vals))
  1501. return changes, tracking_value_ids
  1502. # -------------------------------------------------------------------------
  1503. # RECONCILIATION
  1504. # -------------------------------------------------------------------------
  1505. @api.model
  1506. def _prepare_reconciliation_single_partial(self, debit_vals, credit_vals):
  1507. """ Prepare the values to create an account.partial.reconcile later when reconciling the dictionaries passed
  1508. as parameters, each one representing an account.move.line.
  1509. :param debit_vals: The values of account.move.line to consider for a debit line.
  1510. :param credit_vals: The values of account.move.line to consider for a credit line.
  1511. :return: A dictionary:
  1512. * debit_vals: None if the line has nothing left to reconcile.
  1513. * credit_vals: None if the line has nothing left to reconcile.
  1514. * partial_vals: The newly computed values for the partial.
  1515. """
  1516. def is_payment(vals):
  1517. return vals.get('is_payment') or (
  1518. vals.get('record')
  1519. and bool(vals['record'].move_id.payment_id or vals['record'].move_id.statement_line_id)
  1520. )
  1521. def get_odoo_rate(vals, other_line=None):
  1522. if vals.get('record') and vals['record'].move_id.is_invoice(include_receipts=True):
  1523. exchange_rate_date = vals['record'].move_id.invoice_date
  1524. else:
  1525. exchange_rate_date = vals['date']
  1526. if not is_payment(vals) and other_line and is_payment(other_line):
  1527. exchange_rate_date = other_line['date']
  1528. return recon_currency._get_conversion_rate(company_currency, recon_currency, vals['company'], exchange_rate_date)
  1529. def get_accounting_rate(vals):
  1530. if company_currency.is_zero(vals['balance']) or vals['currency'].is_zero(vals['amount_currency']):
  1531. return None
  1532. else:
  1533. return abs(vals['amount_currency']) / abs(vals['balance'])
  1534. # ==== Determine the currency in which the reconciliation will be done ====
  1535. # In this part, we retrieve the residual amounts, check if they are zero or not and determine in which
  1536. # currency and at which rate the reconciliation will be done.
  1537. res = {
  1538. 'debit_vals': debit_vals,
  1539. 'credit_vals': credit_vals,
  1540. }
  1541. remaining_debit_amount_curr = debit_vals['amount_residual_currency']
  1542. remaining_credit_amount_curr = credit_vals['amount_residual_currency']
  1543. remaining_debit_amount = debit_vals['amount_residual']
  1544. remaining_credit_amount = credit_vals['amount_residual']
  1545. company_currency = debit_vals['company'].currency_id
  1546. has_debit_zero_residual = company_currency.is_zero(remaining_debit_amount)
  1547. has_credit_zero_residual = company_currency.is_zero(remaining_credit_amount)
  1548. has_debit_zero_residual_currency = debit_vals['currency'].is_zero(remaining_debit_amount_curr)
  1549. has_credit_zero_residual_currency = credit_vals['currency'].is_zero(remaining_credit_amount_curr)
  1550. is_rec_pay_account = debit_vals.get('record') \
  1551. and debit_vals['record'].account_type in ('asset_receivable', 'liability_payable')
  1552. if debit_vals['currency'] == credit_vals['currency'] == company_currency \
  1553. and not has_debit_zero_residual \
  1554. and not has_credit_zero_residual:
  1555. # Everything is expressed in company's currency and there is something left to reconcile.
  1556. recon_currency = company_currency
  1557. debit_rate = credit_rate = 1.0
  1558. recon_debit_amount = remaining_debit_amount
  1559. recon_credit_amount = -remaining_credit_amount
  1560. elif debit_vals['currency'] == company_currency \
  1561. and is_rec_pay_account \
  1562. and not has_debit_zero_residual \
  1563. and credit_vals['currency'] != company_currency \
  1564. and not has_credit_zero_residual_currency:
  1565. # The credit line is using a foreign currency but not the opposite line.
  1566. # In that case, convert the amount in company currency to the foreign currency one.
  1567. recon_currency = credit_vals['currency']
  1568. debit_rate = get_odoo_rate(debit_vals, other_line=credit_vals)
  1569. credit_rate = get_accounting_rate(credit_vals)
  1570. recon_debit_amount = recon_currency.round(remaining_debit_amount * debit_rate)
  1571. recon_credit_amount = -remaining_credit_amount_curr
  1572. # If there is nothing left after applying the rate to reconcile in foreign currency,
  1573. # try to fallback on the company currency instead.
  1574. if recon_currency.is_zero(recon_debit_amount) or recon_currency.is_zero(recon_credit_amount):
  1575. recon_currency = company_currency
  1576. debit_rate = 1
  1577. recon_debit_amount = remaining_debit_amount
  1578. recon_credit_amount = -remaining_credit_amount
  1579. elif debit_vals['currency'] != company_currency \
  1580. and is_rec_pay_account \
  1581. and not has_debit_zero_residual_currency \
  1582. and credit_vals['currency'] == company_currency \
  1583. and not has_credit_zero_residual:
  1584. # The debit line is using a foreign currency but not the opposite line.
  1585. # In that case, convert the amount in company currency to the foreign currency one.
  1586. recon_currency = debit_vals['currency']
  1587. debit_rate = get_accounting_rate(debit_vals)
  1588. credit_rate = get_odoo_rate(credit_vals, other_line=debit_vals)
  1589. recon_debit_amount = remaining_debit_amount_curr
  1590. recon_credit_amount = recon_currency.round(-remaining_credit_amount * credit_rate)
  1591. # If there is nothing left after applying the rate to reconcile in foreign currency,
  1592. # try to fallback on the company currency instead.
  1593. if recon_currency.is_zero(recon_debit_amount) or recon_currency.is_zero(recon_credit_amount):
  1594. recon_currency = company_currency
  1595. credit_rate = 1
  1596. recon_debit_amount = remaining_debit_amount
  1597. recon_credit_amount = -remaining_credit_amount
  1598. elif debit_vals['currency'] == credit_vals['currency'] \
  1599. and debit_vals['currency'] != company_currency \
  1600. and not has_debit_zero_residual_currency \
  1601. and not has_credit_zero_residual_currency:
  1602. # Both lines are sharing the same foreign currency.
  1603. recon_currency = debit_vals['currency']
  1604. debit_rate = get_accounting_rate(debit_vals)
  1605. credit_rate = get_accounting_rate(credit_vals)
  1606. recon_debit_amount = remaining_debit_amount_curr
  1607. recon_credit_amount = -remaining_credit_amount_curr
  1608. elif debit_vals['currency'] == credit_vals['currency'] \
  1609. and debit_vals['currency'] != company_currency \
  1610. and (has_debit_zero_residual_currency or has_credit_zero_residual_currency):
  1611. # Special case for exchange difference lines. In that case, both lines are sharing the same foreign
  1612. # currency but at least one has no amount in foreign currency.
  1613. # In that case, we don't want a rate for the opposite line because the exchange difference is supposed
  1614. # to reduce only the amount in company currency but not the foreign one.
  1615. recon_currency = company_currency
  1616. debit_rate = None
  1617. credit_rate = None
  1618. recon_debit_amount = remaining_debit_amount
  1619. recon_credit_amount = -remaining_credit_amount
  1620. else:
  1621. # Multiple involved foreign currencies. The reconciliation is done using the currency of the company.
  1622. recon_currency = company_currency
  1623. debit_rate = get_accounting_rate(debit_vals)
  1624. credit_rate = get_accounting_rate(credit_vals)
  1625. recon_debit_amount = remaining_debit_amount
  1626. recon_credit_amount = -remaining_credit_amount
  1627. # Check if there is something left to reconcile. Move to the next loop iteration if not.
  1628. skip_reconciliation = False
  1629. if recon_currency.is_zero(recon_debit_amount):
  1630. res['debit_vals'] = None
  1631. skip_reconciliation = True
  1632. if recon_currency.is_zero(recon_credit_amount):
  1633. res['credit_vals'] = None
  1634. skip_reconciliation = True
  1635. if skip_reconciliation:
  1636. return res
  1637. # ==== Match both lines together and compute amounts to reconcile ====
  1638. # Determine which line is fully matched by the other.
  1639. compare_amounts = recon_currency.compare_amounts(recon_debit_amount, recon_credit_amount)
  1640. min_recon_amount = min(recon_debit_amount, recon_credit_amount)
  1641. debit_fully_matched = compare_amounts <= 0
  1642. credit_fully_matched = compare_amounts >= 0
  1643. # ==== Computation of partial amounts ====
  1644. if recon_currency == company_currency:
  1645. # Compute the partial amount expressed in company currency.
  1646. partial_amount = min_recon_amount
  1647. # Compute the partial amount expressed in foreign currency.
  1648. if debit_rate:
  1649. partial_debit_amount_currency = debit_vals['currency'].round(debit_rate * min_recon_amount)
  1650. partial_debit_amount_currency = min(partial_debit_amount_currency, remaining_debit_amount_curr)
  1651. else:
  1652. partial_debit_amount_currency = 0.0
  1653. if credit_rate:
  1654. partial_credit_amount_currency = credit_vals['currency'].round(credit_rate * min_recon_amount)
  1655. partial_credit_amount_currency = min(partial_credit_amount_currency, -remaining_credit_amount_curr)
  1656. else:
  1657. partial_credit_amount_currency = 0.0
  1658. else:
  1659. # recon_currency != company_currency
  1660. # Compute the partial amount expressed in company currency.
  1661. if debit_rate:
  1662. partial_debit_amount = company_currency.round(min_recon_amount / debit_rate)
  1663. partial_debit_amount = min(partial_debit_amount, remaining_debit_amount)
  1664. else:
  1665. partial_debit_amount = 0.0
  1666. if credit_rate:
  1667. partial_credit_amount = company_currency.round(min_recon_amount / credit_rate)
  1668. partial_credit_amount = min(partial_credit_amount, -remaining_credit_amount)
  1669. else:
  1670. partial_credit_amount = 0.0
  1671. partial_amount = min(partial_debit_amount, partial_credit_amount)
  1672. # Compute the partial amount expressed in foreign currency.
  1673. # Take care to handle the case when a line expressed in company currency is mimicking the foreign
  1674. # currency of the opposite line.
  1675. if debit_vals['currency'] == company_currency:
  1676. partial_debit_amount_currency = partial_amount
  1677. else:
  1678. partial_debit_amount_currency = min_recon_amount
  1679. if credit_vals['currency'] == company_currency:
  1680. partial_credit_amount_currency = partial_amount
  1681. else:
  1682. partial_credit_amount_currency = min_recon_amount
  1683. # Computation of the partial exchange difference. You can skip this part using the
  1684. # `no_exchange_difference` context key (when reconciling an exchange difference for example).
  1685. if not self._context.get('no_exchange_difference'):
  1686. exchange_lines_to_fix = self.env['account.move.line']
  1687. amounts_list = []
  1688. if recon_currency == company_currency:
  1689. if debit_fully_matched:
  1690. debit_exchange_amount = remaining_debit_amount_curr - partial_debit_amount_currency
  1691. if not debit_vals['currency'].is_zero(debit_exchange_amount):
  1692. if debit_vals.get('record'):
  1693. exchange_lines_to_fix += debit_vals['record']
  1694. amounts_list.append({'amount_residual_currency': debit_exchange_amount})
  1695. remaining_debit_amount_curr -= debit_exchange_amount
  1696. if credit_fully_matched:
  1697. credit_exchange_amount = remaining_credit_amount_curr + partial_credit_amount_currency
  1698. if not credit_vals['currency'].is_zero(credit_exchange_amount):
  1699. if credit_vals.get('record'):
  1700. exchange_lines_to_fix += credit_vals['record']
  1701. amounts_list.append({'amount_residual_currency': credit_exchange_amount})
  1702. remaining_credit_amount_curr += credit_exchange_amount
  1703. else:
  1704. if debit_fully_matched:
  1705. # Create an exchange difference on the remaining amount expressed in company's currency.
  1706. debit_exchange_amount = remaining_debit_amount - partial_amount
  1707. if not company_currency.is_zero(debit_exchange_amount):
  1708. if debit_vals.get('record'):
  1709. exchange_lines_to_fix += debit_vals['record']
  1710. amounts_list.append({'amount_residual': debit_exchange_amount})
  1711. remaining_debit_amount -= debit_exchange_amount
  1712. if debit_vals['currency'] == company_currency:
  1713. remaining_debit_amount_curr -= debit_exchange_amount
  1714. else:
  1715. # Create an exchange difference ensuring the rate between the residual amounts expressed in
  1716. # both foreign and company's currency is still consistent regarding the rate between
  1717. # 'amount_currency' & 'balance'.
  1718. debit_exchange_amount = partial_debit_amount - partial_amount
  1719. if company_currency.compare_amounts(debit_exchange_amount, 0.0) > 0:
  1720. if debit_vals.get('record'):
  1721. exchange_lines_to_fix += debit_vals['record']
  1722. amounts_list.append({'amount_residual': debit_exchange_amount})
  1723. remaining_debit_amount -= debit_exchange_amount
  1724. if debit_vals['currency'] == company_currency:
  1725. remaining_debit_amount_curr -= debit_exchange_amount
  1726. if credit_fully_matched:
  1727. # Create an exchange difference on the remaining amount expressed in company's currency.
  1728. credit_exchange_amount = remaining_credit_amount + partial_amount
  1729. if not company_currency.is_zero(credit_exchange_amount):
  1730. if credit_vals.get('record'):
  1731. exchange_lines_to_fix += credit_vals['record']
  1732. amounts_list.append({'amount_residual': credit_exchange_amount})
  1733. remaining_credit_amount -= credit_exchange_amount
  1734. if credit_vals['currency'] == company_currency:
  1735. remaining_credit_amount_curr -= credit_exchange_amount
  1736. else:
  1737. # Create an exchange difference ensuring the rate between the residual amounts expressed in
  1738. # both foreign and company's currency is still consistent regarding the rate between
  1739. # 'amount_currency' & 'balance'.
  1740. credit_exchange_amount = partial_amount - partial_credit_amount
  1741. if company_currency.compare_amounts(credit_exchange_amount, 0.0) < 0:
  1742. if credit_vals.get('record'):
  1743. exchange_lines_to_fix += credit_vals['record']
  1744. amounts_list.append({'amount_residual': credit_exchange_amount})
  1745. remaining_credit_amount -= credit_exchange_amount
  1746. if credit_vals['currency'] == company_currency:
  1747. remaining_credit_amount_curr -= credit_exchange_amount
  1748. if exchange_lines_to_fix:
  1749. res['exchange_vals'] = exchange_lines_to_fix._prepare_exchange_difference_move_vals(
  1750. amounts_list,
  1751. exchange_date=max(debit_vals['date'], credit_vals['date']),
  1752. )
  1753. # ==== Create partials ====
  1754. remaining_debit_amount -= partial_amount
  1755. remaining_credit_amount += partial_amount
  1756. remaining_debit_amount_curr -= partial_debit_amount_currency
  1757. remaining_credit_amount_curr += partial_credit_amount_currency
  1758. res['partial_vals'] = {
  1759. 'amount': partial_amount,
  1760. 'debit_amount_currency': partial_debit_amount_currency,
  1761. 'credit_amount_currency': partial_credit_amount_currency,
  1762. 'debit_move_id': debit_vals.get('record') and debit_vals['record'].id,
  1763. 'credit_move_id': credit_vals.get('record') and credit_vals['record'].id,
  1764. }
  1765. debit_vals['amount_residual'] = remaining_debit_amount
  1766. debit_vals['amount_residual_currency'] = remaining_debit_amount_curr
  1767. credit_vals['amount_residual'] = remaining_credit_amount
  1768. credit_vals['amount_residual_currency'] = remaining_credit_amount_curr
  1769. if debit_fully_matched:
  1770. res['debit_vals'] = None
  1771. if credit_fully_matched:
  1772. res['credit_vals'] = None
  1773. return res
  1774. @api.model
  1775. def _prepare_reconciliation_partials(self, vals_list):
  1776. ''' Prepare the partials on the current journal items to perform the reconciliation.
  1777. Note: The order of records in self is important because the journal items will be reconciled using this order.
  1778. :return: a tuple of 1) list of vals for partial reconciliation creation, 2) the list of vals for the exchange difference entries to be created
  1779. '''
  1780. debit_vals_list = iter([x for x in vals_list if x['balance'] > 0.0 or x['amount_currency'] > 0.0])
  1781. credit_vals_list = iter([x for x in vals_list if x['balance'] < 0.0 or x['amount_currency'] < 0.0])
  1782. debit_vals = None
  1783. credit_vals = None
  1784. partials_vals_list = []
  1785. exchange_data = {}
  1786. while True:
  1787. # ==== Find the next available lines ====
  1788. # For performance reasons, the partials are created all at once meaning the residual amounts can't be
  1789. # trusted from one iteration to another. That's the reason why all residual amounts are kept as variables
  1790. # and reduced "manually" every time we append a dictionary to 'partials_vals_list'.
  1791. # Move to the next available debit line.
  1792. if not debit_vals:
  1793. debit_vals = next(debit_vals_list, None)
  1794. if not debit_vals:
  1795. break
  1796. # Move to the next available credit line.
  1797. if not credit_vals:
  1798. credit_vals = next(credit_vals_list, None)
  1799. if not credit_vals:
  1800. break
  1801. # ==== Compute the amounts to reconcile ====
  1802. res = self._prepare_reconciliation_single_partial(debit_vals, credit_vals)
  1803. if res.get('partial_vals'):
  1804. if res.get('exchange_vals'):
  1805. exchange_data[len(partials_vals_list)] = res['exchange_vals']
  1806. partials_vals_list.append(res['partial_vals'])
  1807. if res['debit_vals'] is None:
  1808. debit_vals = None
  1809. if res['credit_vals'] is None:
  1810. credit_vals = None
  1811. return partials_vals_list, exchange_data
  1812. def _create_reconciliation_partials(self):
  1813. '''create the partial reconciliation between all the records in self
  1814. :return: A recordset of account.partial.reconcile.
  1815. '''
  1816. partials_vals_list, exchange_data = self._prepare_reconciliation_partials([
  1817. {
  1818. 'record': line,
  1819. 'balance': line.balance,
  1820. 'amount_currency': line.amount_currency,
  1821. 'amount_residual': line.amount_residual,
  1822. 'amount_residual_currency': line.amount_residual_currency,
  1823. 'company': line.company_id,
  1824. 'currency': line.currency_id,
  1825. 'date': line.date,
  1826. }
  1827. for line in self
  1828. ])
  1829. partials = self.env['account.partial.reconcile'].create(partials_vals_list)
  1830. # ==== Create exchange difference moves ====
  1831. for index, exchange_vals in exchange_data.items():
  1832. partials[index].exchange_move_id = self._create_exchange_difference_move(exchange_vals)
  1833. return partials
  1834. def _prepare_exchange_difference_move_vals(self, amounts_list, company=None, exchange_date=None):
  1835. """ Prepare values to create later the exchange difference journal entry.
  1836. The exchange difference journal entry is there to fix the debit/credit of lines when the journal items are
  1837. fully reconciled in foreign currency.
  1838. :param amounts_list: A list of dict, one for each aml.
  1839. :param company: The company in case there is no aml in self.
  1840. :param exchange_date: Optional date object providing the date to consider for the exchange difference.
  1841. :return: A python dictionary containing:
  1842. * move_vals: A dictionary to be passed to the account.move.create method.
  1843. * to_reconcile: A list of tuple <move_line, sequence> in order to perform the reconciliation after the move
  1844. creation.
  1845. """
  1846. company = self.company_id or company
  1847. if not company:
  1848. return
  1849. journal = company.currency_exchange_journal_id
  1850. expense_exchange_account = company.expense_currency_exchange_account_id
  1851. income_exchange_account = company.income_currency_exchange_account_id
  1852. move_vals = {
  1853. 'move_type': 'entry',
  1854. 'date': max(exchange_date or date.min, company._get_user_fiscal_lock_date() + timedelta(days=1)),
  1855. 'journal_id': journal.id,
  1856. 'line_ids': [],
  1857. 'always_tax_exigible': True,
  1858. }
  1859. to_reconcile = []
  1860. for line, amounts in zip(self, amounts_list):
  1861. move_vals['date'] = max(move_vals['date'], line.date)
  1862. if 'amount_residual' in amounts:
  1863. amount_residual = amounts['amount_residual']
  1864. amount_residual_currency = 0.0
  1865. if line.currency_id == line.company_id.currency_id:
  1866. amount_residual_currency = amount_residual
  1867. amount_residual_to_fix = amount_residual
  1868. if line.company_currency_id.is_zero(amount_residual):
  1869. continue
  1870. elif 'amount_residual_currency' in amounts:
  1871. amount_residual = 0.0
  1872. amount_residual_currency = amounts['amount_residual_currency']
  1873. amount_residual_to_fix = amount_residual_currency
  1874. if line.currency_id.is_zero(amount_residual_currency):
  1875. continue
  1876. else:
  1877. continue
  1878. if amount_residual_to_fix > 0.0:
  1879. exchange_line_account = expense_exchange_account
  1880. else:
  1881. exchange_line_account = income_exchange_account
  1882. sequence = len(move_vals['line_ids'])
  1883. move_vals['line_ids'] += [
  1884. Command.create({
  1885. 'name': _('Currency exchange rate difference'),
  1886. 'debit': -amount_residual if amount_residual < 0.0 else 0.0,
  1887. 'credit': amount_residual if amount_residual > 0.0 else 0.0,
  1888. 'amount_currency': -amount_residual_currency,
  1889. 'account_id': line.account_id.id,
  1890. 'currency_id': line.currency_id.id,
  1891. 'partner_id': line.partner_id.id,
  1892. 'sequence': sequence,
  1893. }),
  1894. Command.create({
  1895. 'name': _('Currency exchange rate difference'),
  1896. 'debit': amount_residual if amount_residual > 0.0 else 0.0,
  1897. 'credit': -amount_residual if amount_residual < 0.0 else 0.0,
  1898. 'amount_currency': amount_residual_currency,
  1899. 'account_id': exchange_line_account.id,
  1900. 'currency_id': line.currency_id.id,
  1901. 'partner_id': line.partner_id.id,
  1902. 'sequence': sequence + 1,
  1903. }),
  1904. ]
  1905. to_reconcile.append((line, sequence))
  1906. return {'move_vals': move_vals, 'to_reconcile': to_reconcile}
  1907. @api.model
  1908. def _create_exchange_difference_move(self, exchange_diff_vals):
  1909. """ Create the exchange difference journal entry on the current journal items.
  1910. :param exchange_diff_vals: The current vals of the exchange difference journal entry created by the
  1911. '_prepare_exchange_difference_move_vals' method.
  1912. :return: An account.move record.
  1913. """
  1914. move_vals = exchange_diff_vals['move_vals']
  1915. if not move_vals['line_ids']:
  1916. return
  1917. # Check the configuration of the exchange difference journal.
  1918. journal = self.env['account.journal'].browse(move_vals['journal_id'])
  1919. if not journal:
  1920. raise UserError(_(
  1921. "You should configure the 'Exchange Gain or Loss Journal' in your company settings, to manage"
  1922. " automatically the booking of accounting entries related to differences between exchange rates."
  1923. ))
  1924. if not journal.company_id.expense_currency_exchange_account_id:
  1925. raise UserError(_(
  1926. "You should configure the 'Loss Exchange Rate Account' in your company settings, to manage"
  1927. " automatically the booking of accounting entries related to differences between exchange rates."
  1928. ))
  1929. if not journal.company_id.income_currency_exchange_account_id.id:
  1930. raise UserError(_(
  1931. "You should configure the 'Gain Exchange Rate Account' in your company settings, to manage"
  1932. " automatically the booking of accounting entries related to differences between exchange rates."
  1933. ))
  1934. # Create the move.
  1935. exchange_move = self.env['account.move'].with_context(skip_invoice_sync=True).create(move_vals)
  1936. exchange_move._post(soft=False)
  1937. # Reconcile lines to the newly created exchange difference journal entry by creating more partials.
  1938. for source_line, sequence in exchange_diff_vals['to_reconcile']:
  1939. exchange_diff_line = exchange_move.line_ids[sequence]
  1940. (exchange_diff_line + source_line).with_context(no_exchange_difference=True).reconcile()
  1941. return exchange_move
  1942. def _add_exchange_difference_cash_basis_vals(self, exchange_diff_vals):
  1943. """ Generate the exchange difference values used to create the journal items
  1944. in order to fix the cash basis lines using the transfer account in a multi-currencies
  1945. environment when this account is not a reconcile one.
  1946. When the tax cash basis journal entries are generated and all involved
  1947. transfer account set on taxes are all reconcilable, the account balance
  1948. will be reset to zero by the exchange difference journal items generated
  1949. above. However, this mechanism will not work if there is any transfer
  1950. accounts that are not reconcile and we are generating the cash basis
  1951. journal items in a foreign currency. In that specific case, we need to
  1952. generate extra journal items at the generation of the exchange difference
  1953. journal entry to ensure this balance is reset to zero and then, will not
  1954. appear on the tax report leading to erroneous tax base amount / tax amount.
  1955. :param exchange_diff_vals: The current vals of the exchange difference journal entry created by the
  1956. '_prepare_exchange_difference_move_vals' method.
  1957. """
  1958. caba_lines_to_reconcile = defaultdict(lambda: self.env['account.move.line']) # in the form {(move, account, repartition_line): move_lines}
  1959. move_vals = exchange_diff_vals['move_vals']
  1960. for move in self.move_id:
  1961. account_vals_to_fix = {}
  1962. move_values = move._collect_tax_cash_basis_values()
  1963. # The cash basis doesn't need to be handled for this move because there is another payment term
  1964. # line that is not yet fully paid.
  1965. if not move_values or not move_values['is_fully_paid']:
  1966. continue
  1967. # ==========================================================================
  1968. # Add the balance of all tax lines of the current move in order in order
  1969. # to compute the residual amount for each of them.
  1970. # ==========================================================================
  1971. caba_rounding_diff_label = _("Cash basis rounding difference")
  1972. move_vals['date'] = max(move_vals['date'], move.date)
  1973. for caba_treatment, line in move_values['to_process_lines']:
  1974. vals = {
  1975. 'name': caba_rounding_diff_label,
  1976. 'currency_id': line.currency_id.id,
  1977. 'partner_id': line.partner_id.id,
  1978. 'tax_ids': [Command.set(line.tax_ids.ids)],
  1979. 'tax_tag_ids': [Command.set(line.tax_tag_ids.ids)],
  1980. 'debit': line.debit,
  1981. 'credit': line.credit,
  1982. 'amount_currency': line.amount_currency,
  1983. }
  1984. if caba_treatment == 'tax':
  1985. # Tax line.
  1986. grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line)
  1987. if grouping_key in account_vals_to_fix:
  1988. debit = account_vals_to_fix[grouping_key]['debit'] + vals['debit']
  1989. credit = account_vals_to_fix[grouping_key]['credit'] + vals['credit']
  1990. balance = debit - credit
  1991. account_vals_to_fix[grouping_key].update({
  1992. 'debit': balance if balance > 0 else 0,
  1993. 'credit': -balance if balance < 0 else 0,
  1994. 'tax_base_amount': account_vals_to_fix[grouping_key]['tax_base_amount'] + line.tax_base_amount,
  1995. 'amount_currency': account_vals_to_fix[grouping_key]['amount_currency'] + line.amount_currency,
  1996. })
  1997. else:
  1998. account_vals_to_fix[grouping_key] = {
  1999. **vals,
  2000. 'account_id': line.account_id.id,
  2001. 'tax_base_amount': line.tax_base_amount,
  2002. 'tax_repartition_line_id': line.tax_repartition_line_id.id,
  2003. }
  2004. if line.account_id.reconcile:
  2005. caba_lines_to_reconcile[(move, line.account_id, line.tax_repartition_line_id)] |= line
  2006. elif caba_treatment == 'base':
  2007. # Base line.
  2008. account_to_fix = line.company_id.account_cash_basis_base_account_id
  2009. if not account_to_fix:
  2010. continue
  2011. grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(line, account=account_to_fix)
  2012. if grouping_key not in account_vals_to_fix:
  2013. account_vals_to_fix[grouping_key] = {
  2014. **vals,
  2015. 'account_id': account_to_fix.id,
  2016. }
  2017. else:
  2018. # Multiple base lines could share the same key, if the same
  2019. # cash basis tax is used alone on several lines of the invoices
  2020. account_vals_to_fix[grouping_key]['debit'] += vals['debit']
  2021. account_vals_to_fix[grouping_key]['credit'] += vals['credit']
  2022. account_vals_to_fix[grouping_key]['amount_currency'] += vals['amount_currency']
  2023. # ==========================================================================
  2024. # Subtract the balance of all previously generated cash basis journal entries
  2025. # in order to retrieve the residual balance of each involved transfer account.
  2026. # ==========================================================================
  2027. cash_basis_moves = self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', move.id)])
  2028. caba_transition_accounts = self.env['account.account']
  2029. for line in cash_basis_moves.line_ids:
  2030. grouping_key = None
  2031. if line.tax_repartition_line_id:
  2032. # Tax line.
  2033. transition_account = line.tax_line_id.cash_basis_transition_account_id
  2034. grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(
  2035. line,
  2036. account=transition_account,
  2037. )
  2038. caba_transition_accounts |= transition_account
  2039. elif line.tax_ids:
  2040. # Base line.
  2041. grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(
  2042. line,
  2043. account=line.company_id.account_cash_basis_base_account_id,
  2044. )
  2045. if grouping_key not in account_vals_to_fix:
  2046. continue
  2047. account_vals_to_fix[grouping_key]['debit'] -= line.debit
  2048. account_vals_to_fix[grouping_key]['credit'] -= line.credit
  2049. account_vals_to_fix[grouping_key]['amount_currency'] -= line.amount_currency
  2050. # Collect the caba lines affecting the transition account.
  2051. for transition_line in filter(lambda x: x.account_id in caba_transition_accounts, cash_basis_moves.line_ids):
  2052. caba_reconcile_key = (transition_line.move_id, transition_line.account_id, transition_line.tax_repartition_line_id)
  2053. caba_lines_to_reconcile[caba_reconcile_key] |= transition_line
  2054. # ==========================================================================
  2055. # Generate the exchange difference journal items:
  2056. # - to reset the balance of all transfer account to zero.
  2057. # - fix rounding issues on the tax account/base tax account.
  2058. # ==========================================================================
  2059. currency = move_values['currency']
  2060. # To know which rate to use for the adjustment, get the rate used by the most recent cash basis move
  2061. last_caba_move = max(cash_basis_moves, key=lambda m: m.date) if cash_basis_moves else self.env['account.move']
  2062. currency_line = last_caba_move.line_ids.filtered(lambda x: x.currency_id == currency)[:1]
  2063. currency_rate = currency_line.balance / currency_line.amount_currency if currency_line.amount_currency else 1.0
  2064. existing_line_vals_list = move_vals['line_ids']
  2065. next_sequence = len(existing_line_vals_list)
  2066. for grouping_key, values in account_vals_to_fix.items():
  2067. if currency.is_zero(values['amount_currency']):
  2068. continue
  2069. # There is a rounding error due to multiple payments on the foreign currency amount
  2070. balance = currency.round(currency_rate * values['amount_currency'])
  2071. if values.get('tax_repartition_line_id'):
  2072. # Tax line
  2073. tax_repartition_line = self.env['account.tax.repartition.line'].browse(values['tax_repartition_line_id'])
  2074. account = tax_repartition_line.account_id or self.env['account.account'].browse(values['account_id'])
  2075. existing_line_vals_list.extend([
  2076. Command.create({
  2077. **values,
  2078. 'debit': balance if balance > 0.0 else 0.0,
  2079. 'credit': -balance if balance < 0.0 else 0.0,
  2080. 'amount_currency': values['amount_currency'],
  2081. 'account_id': account.id,
  2082. 'sequence': next_sequence,
  2083. }),
  2084. Command.create({
  2085. **values,
  2086. 'debit': -balance if balance < 0.0 else 0.0,
  2087. 'credit': balance if balance > 0.0 else 0.0,
  2088. 'amount_currency': -values['amount_currency'],
  2089. 'account_id': values['account_id'],
  2090. 'tax_ids': [],
  2091. 'tax_tag_ids': [],
  2092. 'tax_base_amount': 0,
  2093. 'tax_repartition_line_id': False,
  2094. 'sequence': next_sequence + 1,
  2095. }),
  2096. ])
  2097. else:
  2098. # Base line
  2099. existing_line_vals_list.extend([
  2100. Command.create({
  2101. **values,
  2102. 'debit': balance if balance > 0.0 else 0.0,
  2103. 'credit': -balance if balance < 0.0 else 0.0,
  2104. 'amount_currency': values['amount_currency'],
  2105. 'sequence': next_sequence,
  2106. }),
  2107. Command.create({
  2108. **values,
  2109. 'debit': -balance if balance < 0.0 else 0.0,
  2110. 'credit': balance if balance > 0.0 else 0.0,
  2111. 'amount_currency': -values['amount_currency'],
  2112. 'tax_ids': [],
  2113. 'tax_tag_ids': [],
  2114. 'sequence': next_sequence + 1,
  2115. }),
  2116. ])
  2117. next_sequence += 2
  2118. return caba_lines_to_reconcile
  2119. def reconcile(self):
  2120. ''' Reconcile the current move lines all together.
  2121. :return: A dictionary representing a summary of what has been done during the reconciliation:
  2122. * partials: A recorset of all account.partial.reconcile created during the reconciliation.
  2123. * exchange_partials: A recorset of all account.partial.reconcile created during the reconciliation
  2124. with the exchange difference journal entries.
  2125. * full_reconcile: An account.full.reconcile record created when there is nothing left to reconcile
  2126. in the involved lines.
  2127. * tax_cash_basis_moves: An account.move recordset representing the tax cash basis journal entries.
  2128. '''
  2129. results = {'exchange_partials': self.env['account.partial.reconcile']}
  2130. if not self:
  2131. return results
  2132. not_paid_invoices = self.move_id.filtered(lambda move:
  2133. move.is_invoice(include_receipts=True)
  2134. and move.payment_state not in ('paid', 'in_payment')
  2135. )
  2136. # ==== Check the lines can be reconciled together ====
  2137. company = None
  2138. account = None
  2139. for line in self:
  2140. if line.reconciled:
  2141. raise UserError(_("You are trying to reconcile some entries that are already reconciled."))
  2142. if not line.account_id.reconcile and line.account_id.account_type not in ('asset_cash', 'liability_credit_card'):
  2143. raise UserError(_("Account %s does not allow reconciliation. First change the configuration of this account to allow it.")
  2144. % line.account_id.display_name)
  2145. if line.move_id.state != 'posted':
  2146. raise UserError(_('You can only reconcile posted entries.'))
  2147. if company is None:
  2148. company = line.company_id
  2149. elif line.company_id != company:
  2150. raise UserError(_("Entries doesn't belong to the same company: %s != %s")
  2151. % (company.display_name, line.company_id.display_name))
  2152. if account is None:
  2153. account = line.account_id
  2154. elif line.account_id != account:
  2155. raise UserError(_("Entries are not from the same account: %s != %s")
  2156. % (account.display_name, line.account_id.display_name))
  2157. if self._context.get('reduced_line_sorting'):
  2158. sorting_f = lambda line: (line.date_maturity or line.date, line.currency_id)
  2159. else:
  2160. sorting_f = lambda line: (line.date_maturity or line.date, line.currency_id, line.amount_currency)
  2161. sorted_lines = self.sorted(key=sorting_f)
  2162. # ==== Collect all involved lines through the existing reconciliation ====
  2163. involved_lines = sorted_lines._all_reconciled_lines()
  2164. involved_partials = involved_lines.matched_credit_ids | involved_lines.matched_debit_ids
  2165. # ==== Create partials ====
  2166. partial_no_exch_diff = bool(self.env['ir.config_parameter'].sudo().get_param('account.disable_partial_exchange_diff'))
  2167. sorted_lines_ctx = sorted_lines.with_context(no_exchange_difference=self._context.get('no_exchange_difference') or partial_no_exch_diff)
  2168. partials = sorted_lines_ctx._create_reconciliation_partials()
  2169. results['partials'] = partials
  2170. involved_partials += partials
  2171. exchange_move_lines = partials.exchange_move_id.line_ids.filtered(lambda line: line.account_id == account)
  2172. involved_lines += exchange_move_lines
  2173. exchange_diff_partials = exchange_move_lines.matched_debit_ids + exchange_move_lines.matched_credit_ids
  2174. involved_partials += exchange_diff_partials
  2175. results['exchange_partials'] += exchange_diff_partials
  2176. # ==== Create entries for cash basis taxes ====
  2177. is_cash_basis_needed = account.company_id.tax_exigibility and account.account_type in ('asset_receivable', 'liability_payable')
  2178. if is_cash_basis_needed and not self._context.get('move_reverse_cancel') and not self._context.get('no_cash_basis'):
  2179. tax_cash_basis_moves = partials._create_tax_cash_basis_moves()
  2180. results['tax_cash_basis_moves'] = tax_cash_basis_moves
  2181. # ==== Check if a full reconcile is needed ====
  2182. def is_line_reconciled(line, has_multiple_currencies):
  2183. # Check if the journal item passed as parameter is now fully reconciled.
  2184. return line.reconciled \
  2185. or (line.company_currency_id.is_zero(line.amount_residual)
  2186. if has_multiple_currencies
  2187. else line.currency_id.is_zero(line.amount_residual_currency)
  2188. )
  2189. has_multiple_currencies = len(involved_lines.currency_id) > 1
  2190. if all(is_line_reconciled(line, has_multiple_currencies) for line in involved_lines):
  2191. # ==== Create the exchange difference move ====
  2192. # This part could be bypassed using the 'no_exchange_difference' key inside the context. This is useful
  2193. # when importing a full accounting including the reconciliation like Winbooks.
  2194. exchange_move = self.env['account.move']
  2195. caba_lines_to_reconcile = None
  2196. if not self._context.get('no_exchange_difference'):
  2197. # In normal cases, the exchange differences are already generated by the partial at this point meaning
  2198. # there is no journal item left with a zero amount residual in one currency but not in the other.
  2199. # However, after a migration coming from an older version with an older partial reconciliation or due to
  2200. # some rounding issues (when dealing with different decimal places for example), we could need an extra
  2201. # exchange difference journal entry to handle them.
  2202. exchange_lines_to_fix = self.env['account.move.line']
  2203. amounts_list = []
  2204. exchange_max_date = date.min
  2205. for line in involved_lines:
  2206. if not line.company_currency_id.is_zero(line.amount_residual):
  2207. exchange_lines_to_fix += line
  2208. amounts_list.append({'amount_residual': line.amount_residual})
  2209. elif not line.currency_id.is_zero(line.amount_residual_currency):
  2210. exchange_lines_to_fix += line
  2211. amounts_list.append({'amount_residual_currency': line.amount_residual_currency})
  2212. exchange_max_date = max(exchange_max_date, line.date)
  2213. exchange_diff_vals = exchange_lines_to_fix._prepare_exchange_difference_move_vals(
  2214. amounts_list,
  2215. company=involved_lines[0].company_id,
  2216. exchange_date=exchange_max_date,
  2217. )
  2218. # Exchange difference for cash basis entries.
  2219. # If we are fully reversing the entry, no need to fix anything since the journal entry
  2220. # is exactly the mirror of the source journal entry.
  2221. if is_cash_basis_needed and not self._context.get('move_reverse_cancel'):
  2222. caba_lines_to_reconcile = involved_lines._add_exchange_difference_cash_basis_vals(exchange_diff_vals)
  2223. # Create the exchange difference.
  2224. if exchange_diff_vals['move_vals']['line_ids']:
  2225. exchange_move = involved_lines._create_exchange_difference_move(exchange_diff_vals)
  2226. if exchange_move:
  2227. exchange_move_lines = exchange_move.line_ids.filtered(lambda line: line.account_id == account)
  2228. # Track newly created lines.
  2229. involved_lines += exchange_move_lines
  2230. # Track newly created partials.
  2231. exchange_diff_partials = exchange_move_lines.matched_debit_ids \
  2232. + exchange_move_lines.matched_credit_ids
  2233. involved_partials += exchange_diff_partials
  2234. results['exchange_partials'] += exchange_diff_partials
  2235. # ==== Create the full reconcile ====
  2236. results['full_reconcile'] = self.env['account.full.reconcile'] \
  2237. .with_context(
  2238. skip_invoice_sync=True,
  2239. skip_invoice_line_sync=True,
  2240. skip_account_move_synchronization=True,
  2241. check_move_validity=False,
  2242. ) \
  2243. .create({
  2244. 'exchange_move_id': exchange_move and exchange_move.id,
  2245. 'partial_reconcile_ids': [Command.set(involved_partials.ids)],
  2246. 'reconciled_line_ids': [Command.set(involved_lines.ids)],
  2247. })
  2248. # === Cash basis rounding autoreconciliation ===
  2249. # In case a cash basis rounding difference line got created for the transition account, we reconcile it with the corresponding lines
  2250. # on the cash basis moves (so that it reaches full reconciliation and creates an exchange difference entry for this account as well)
  2251. if caba_lines_to_reconcile:
  2252. for (dummy, account, repartition_line), amls_to_reconcile in caba_lines_to_reconcile.items():
  2253. if not account.reconcile:
  2254. continue
  2255. exchange_line = exchange_move.line_ids.filtered(
  2256. lambda l: l.account_id == account and l.tax_repartition_line_id == repartition_line
  2257. )
  2258. (exchange_line + amls_to_reconcile).filtered(lambda l: not l.reconciled).reconcile()
  2259. not_paid_invoices.filtered(lambda move:
  2260. move.payment_state in ('paid', 'in_payment')
  2261. )._invoice_paid_hook()
  2262. return results
  2263. def remove_move_reconcile(self):
  2264. """ Undo a reconciliation """
  2265. (self.matched_debit_ids + self.matched_credit_ids).unlink()
  2266. # -------------------------------------------------------------------------
  2267. # ANALYTIC
  2268. # -------------------------------------------------------------------------
  2269. def _validate_analytic_distribution(self):
  2270. for line in self.filtered(lambda line: line.display_type == 'product'):
  2271. line._validate_distribution(**{
  2272. 'product': line.product_id.id,
  2273. 'account': line.account_id.id,
  2274. 'business_domain': line.move_id.move_type in ['out_invoice', 'out_refund', 'out_receipt'] and 'invoice'
  2275. or line.move_id.move_type in ['in_invoice', 'in_refund', 'in_receipt'] and 'bill'
  2276. or 'general',
  2277. 'company_id': line.company_id.id,
  2278. })
  2279. def _create_analytic_lines(self):
  2280. """ Create analytic items upon validation of an account.move.line having an analytic distribution.
  2281. """
  2282. self._validate_analytic_distribution()
  2283. analytic_line_vals = []
  2284. for line in self:
  2285. analytic_line_vals.extend(line._prepare_analytic_lines())
  2286. self.env['account.analytic.line'].create(analytic_line_vals)
  2287. def _prepare_analytic_lines(self):
  2288. self.ensure_one()
  2289. analytic_line_vals = []
  2290. if self.analytic_distribution:
  2291. # distribution_on_each_plan corresponds to the proportion that is distributed to each plan to be able to
  2292. # give the real amount when we achieve a 100% distribution
  2293. distribution_on_each_plan = {}
  2294. for account_id, distribution in self.analytic_distribution.items():
  2295. line_values = self._prepare_analytic_distribution_line(float(distribution), account_id, distribution_on_each_plan)
  2296. if not self.currency_id.is_zero(line_values.get('amount')):
  2297. analytic_line_vals.append(line_values)
  2298. return analytic_line_vals
  2299. def _prepare_analytic_distribution_line(self, distribution, account_id, distribution_on_each_plan):
  2300. """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
  2301. analytic tags with analytic distribution.
  2302. """
  2303. self.ensure_one()
  2304. account_id = int(account_id)
  2305. account = self.env['account.analytic.account'].browse(account_id)
  2306. distribution_plan = distribution_on_each_plan.get(account.root_plan_id, 0) + distribution
  2307. decimal_precision = self.env['decimal.precision'].precision_get('Percentage Analytic')
  2308. if float_compare(distribution_plan, 100, precision_digits=decimal_precision) == 0:
  2309. amount = -self.balance * (100 - distribution_on_each_plan.get(account.root_plan_id, 0)) / 100.0
  2310. else:
  2311. amount = -self.balance * distribution / 100.0
  2312. distribution_on_each_plan[account.root_plan_id] = distribution_plan
  2313. default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/'))
  2314. return {
  2315. 'name': default_name,
  2316. 'date': self.date,
  2317. 'account_id': account_id,
  2318. 'partner_id': self.partner_id.id,
  2319. 'unit_amount': self.quantity,
  2320. 'product_id': self.product_id and self.product_id.id or False,
  2321. 'product_uom_id': self.product_uom_id and self.product_uom_id.id or False,
  2322. 'amount': amount,
  2323. 'general_account_id': self.account_id.id,
  2324. 'ref': self.ref,
  2325. 'move_line_id': self.id,
  2326. 'user_id': self.move_id.invoice_user_id.id or self._uid,
  2327. 'company_id': account.company_id.id or self.company_id.id or self.env.company.id,
  2328. 'category': 'invoice' if self.move_id.is_sale_document() else 'vendor_bill' if self.move_id.is_purchase_document() else 'other',
  2329. }
  2330. # -------------------------------------------------------------------------
  2331. # MISC
  2332. # -------------------------------------------------------------------------
  2333. def _get_integrity_hash_fields(self):
  2334. # Use the new hash version by default, but keep the old one for backward compatibility when generating the integrity report.
  2335. hash_version = self._context.get('hash_version', MAX_HASH_VERSION)
  2336. if hash_version == 1:
  2337. return ['debit', 'credit', 'account_id', 'partner_id']
  2338. elif hash_version in (2, 3):
  2339. return ['name', 'debit', 'credit', 'account_id', 'partner_id']
  2340. raise NotImplementedError(f"hash_version={hash_version} doesn't exist")
  2341. def _reconciled_lines(self):
  2342. ids = []
  2343. for aml in self.filtered('reconciled'):
  2344. ids.extend([r.debit_move_id.id for r in aml.matched_debit_ids] if aml.credit > 0 else [r.credit_move_id.id for r in aml.matched_credit_ids])
  2345. ids.append(aml.id)
  2346. return ids
  2347. def _all_reconciled_lines(self):
  2348. reconciliation_lines = self.filtered(lambda x: x.account_id.reconcile or x.account_id.account_type in ('asset_cash', 'liability_credit_card'))
  2349. current_lines = reconciliation_lines
  2350. current_partials = self.env['account.partial.reconcile']
  2351. while current_lines:
  2352. current_partials = (current_lines.matched_debit_ids + current_lines.matched_credit_ids) - current_partials
  2353. current_lines = (current_partials.debit_move_id + current_partials.credit_move_id) - current_lines
  2354. reconciliation_lines += current_lines
  2355. return reconciliation_lines
  2356. def _get_attachment_domains(self):
  2357. self.ensure_one()
  2358. domains = [[('res_model', '=', 'account.move'), ('res_id', '=', self.move_id.id)]]
  2359. if self.statement_id:
  2360. domains.append([('res_model', '=', 'account.bank.statement'), ('res_id', '=', self.statement_id.id)])
  2361. if self.payment_id:
  2362. domains.append([('res_model', '=', 'account.payment'), ('res_id', '=', self.payment_id.id)])
  2363. return domains
  2364. @api.model
  2365. def _get_tax_exigible_domain(self):
  2366. """ Returns a domain to be used to identify the move lines that are allowed
  2367. to be taken into account in the tax report.
  2368. """
  2369. return [
  2370. # Lines on moves without any payable or receivable line are always exigible
  2371. '|', ('move_id.always_tax_exigible', '=', True),
  2372. # Lines with only tags are always exigible
  2373. '|', '&', ('tax_line_id', '=', False), ('tax_ids', '=', False),
  2374. # Lines from CABA entries are always exigible
  2375. '|', ('move_id.tax_cash_basis_rec_id', '!=', False),
  2376. # Lines from non-CABA taxes are always exigible
  2377. '|', ('tax_line_id.tax_exigibility', '!=', 'on_payment'),
  2378. ('tax_ids.tax_exigibility', '!=', 'on_payment'), # So: exigible if at least one tax from tax_ids isn't on_payment
  2379. ]
  2380. def _convert_to_tax_base_line_dict(self):
  2381. """ Convert the current record to a dictionary in order to use the generic taxes computation method
  2382. defined on account.tax.
  2383. :return: A python dictionary.
  2384. """
  2385. self.ensure_one()
  2386. is_invoice = self.move_id.is_invoice(include_receipts=True)
  2387. sign = -1 if self.move_id.is_inbound(include_receipts=True) else 1
  2388. return self.env['account.tax']._convert_to_tax_base_line_dict(
  2389. self,
  2390. partner=self.partner_id,
  2391. currency=self.currency_id,
  2392. product=self.product_id,
  2393. taxes=self.tax_ids,
  2394. price_unit=self.price_unit if is_invoice else self.amount_currency,
  2395. quantity=self.quantity if is_invoice else 1.0,
  2396. discount=self.discount if is_invoice else 0.0,
  2397. account=self.account_id,
  2398. analytic_distribution=self.analytic_distribution,
  2399. price_subtotal=sign * self.amount_currency,
  2400. is_refund=self.is_refund,
  2401. rate=(abs(self.amount_currency) / abs(self.balance)) if self.balance else 1.0
  2402. )
  2403. def _convert_to_tax_line_dict(self):
  2404. """ Convert the current record to a dictionary in order to use the generic taxes computation method
  2405. defined on account.tax.
  2406. :return: A python dictionary.
  2407. """
  2408. self.ensure_one()
  2409. sign = -1 if self.move_id.is_inbound(include_receipts=True) else 1
  2410. return self.env['account.tax']._convert_to_tax_line_dict(
  2411. self,
  2412. partner=self.partner_id,
  2413. currency=self.currency_id,
  2414. taxes=self.tax_ids,
  2415. tax_tags=self.tax_tag_ids,
  2416. tax_repartition_line=self.tax_repartition_line_id,
  2417. group_tax=self.group_tax_id,
  2418. account=self.account_id,
  2419. analytic_distribution=self.analytic_distribution,
  2420. tax_amount=sign * self.amount_currency,
  2421. )
  2422. def _get_invoiced_qty_per_product(self):
  2423. qties = defaultdict(float)
  2424. for aml in self:
  2425. qty = aml.product_uom_id._compute_quantity(aml.quantity, aml.product_id.uom_id)
  2426. if aml.move_id.move_type == 'out_invoice':
  2427. qties[aml.product_id] += qty
  2428. elif aml.move_id.move_type == 'out_refund':
  2429. qties[aml.product_id] -= qty
  2430. return qties
  2431. def _get_lock_date_protected_fields(self):
  2432. """ Returns the names of the fields that should be protected by the accounting fiscal year and tax lock dates
  2433. """
  2434. tax_fnames = ['balance', 'tax_line_id', 'tax_ids', 'tax_tag_ids']
  2435. fiscal_fnames = tax_fnames + ['account_id', 'journal_id', 'amount_currency', 'currency_id', 'partner_id']
  2436. reconciliation_fnames = ['account_id', 'date', 'balance', 'amount_currency', 'currency_id', 'partner_id']
  2437. return {
  2438. 'tax': tax_fnames,
  2439. 'fiscal': fiscal_fnames,
  2440. 'reconciliation': reconciliation_fnames,
  2441. }
  2442. @api.model
  2443. def get_import_templates(self):
  2444. return [{
  2445. 'label': _('Import Template for Journal Items'),
  2446. 'template': '/account/static/xls/aml_import_template.xlsx'
  2447. }]
  2448. def _is_eligible_for_early_payment_discount(self, currency, reference_date):
  2449. self.ensure_one()
  2450. return self.display_type == 'payment_term' \
  2451. and self.currency_id == currency \
  2452. and self.move_id.move_type in ('out_invoice', 'out_receipt', 'in_invoice', 'in_receipt') \
  2453. and not self.matched_debit_ids \
  2454. and not self.matched_credit_ids \
  2455. and self.discount_date \
  2456. and reference_date <= self.discount_date
  2457. # -------------------------------------------------------------------------
  2458. # PUBLIC ACTIONS
  2459. # -------------------------------------------------------------------------
  2460. def open_reconcile_view(self):
  2461. action = self.env['ir.actions.act_window']._for_xml_id('account.action_account_moves_all_grouped_matching')
  2462. ids = self._all_reconciled_lines().filtered(lambda l: l.matched_debit_ids or l.matched_credit_ids).ids
  2463. action['domain'] = [('id', 'in', ids)]
  2464. return clean_action(action, self.env)
  2465. def action_open_business_doc(self):
  2466. return self.move_id.action_open_business_doc()
  2467. def action_automatic_entry(self):
  2468. action = self.env['ir.actions.act_window']._for_xml_id('account.account_automatic_entry_wizard_action')
  2469. # Force the values of the move line in the context to avoid issues
  2470. ctx = dict(self.env.context)
  2471. ctx.pop('active_id', None)
  2472. ctx.pop('default_journal_id', None)
  2473. ctx['active_ids'] = self.ids
  2474. ctx['active_model'] = 'account.move.line'
  2475. action['context'] = ctx
  2476. return action
  2477. # -------------------------------------------------------------------------
  2478. # TOOLING
  2479. # -------------------------------------------------------------------------
  2480. def _conditional_add_to_compute(self, fname, condition):
  2481. field = self._fields[fname]
  2482. to_reset = self.filtered(lambda line:
  2483. condition(line)
  2484. and not self.env.is_protected(field, line)
  2485. )
  2486. to_reset.invalidate_recordset([fname])
  2487. self.env.add_to_compute(field, to_reset)
  2488. # -------------------------------------------------------------------------
  2489. # HOOKS
  2490. # -------------------------------------------------------------------------
  2491. def _copy_data_extend_business_fields(self, values):
  2492. self.ensure_one()
  2493. def _get_downpayment_lines(self):
  2494. ''' Return the downpayment move lines associated with the move line.
  2495. This method is overridden in the sale order module.
  2496. '''
  2497. return self.env['account.move.line']