diff --git a/DEMO_trip_suite.md b/DEMO_trip_suite.md new file mode 100644 index 000000000..66b1c07d7 --- /dev/null +++ b/DEMO_trip_suite.md @@ -0,0 +1,43 @@ +# HR Expense — Trip suite demo (SMB sales-rep walk) + +This branch stacks the optional **Trip** layer over the base hr-expense flow so it +can be installed + evaluated as one unit: + +- `hr_expense_trip` — the optional Trip container (groups expense lines; approval + workflow draft → request → approve → collect receipts → done; trip PDF). +- `hr_expense_advance_clearing` + `hr_expense_advance_clearing_trip` — a **trip + advance = the budget** (cash issued for the trip, cleared against actual spend). +- `hr_expense_payment` + `hr_expense_payment_trip` — **reimburse the whole trip + in one payment**. + +The Trip is **opt-in**: nothing changes about the plain per-expense flow unless a +trip is used. + +## Scenario — a sales rep on the road + +**Without a trip (base flow):** the rep files each expense individually (flight, +hotel, dinner). Each is approved and reimbursed on its own; there is no budget and +no grouping — fine for the occasional expense, noisy for a multi-day trip. + +**With a trip:** the rep's manager opens a **Trip** ("Q1 East Coast Sales Tour"), +issues a **$2,000 advance** as the trip budget, and approves the trip. The rep +collects receipts against the trip; **Apply Advance** clears them against the +budget (Advance Remaining tracks what's left), and **Pay Trip** reimburses any +balance in one payment. One approval, one budget, one settlement. + +| Need | Base | Trip | +|---|---|---| +| Group a multi-day trip's expenses | — | trip.expense_ids | +| Budget | — | advance ( $ issued up front ) | +| Approval | per expense | one trip approval workflow | +| Reimburse | per expense | one payment (Pay Trip) | +| Document | per expense | trip PDF report | + +## Screenshots +- Trip with advance budget + Apply Advance ([advance demo](https://storage.googleapis.com/ledo-pr-assets/hr-expense/pr-3/advance-clearing-trip.png)). +- Trip form + batched expenses + state machine ([trip foundation](https://storage.googleapis.com/ledo-pr-assets/hr-expense/pr-1/01-buffer-form-flow-index.png) — see hr-expense#1 for the trip walk). + +## Eval +Open this branch's runboat build (admin/admin) and follow the scenario above. +Note: in 19.0, expense posting routes through `hr.expense.post.wizard`, so for the +**Pay Trip** step do *Create Bill* → confirm the post wizard → **Pay Trip**. diff --git a/hr_expense_advance_clearing/README.rst b/hr_expense_advance_clearing/README.rst new file mode 100644 index 000000000..cef685f04 --- /dev/null +++ b/hr_expense_advance_clearing/README.rst @@ -0,0 +1,187 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================= +Employee Advance and Clearing +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3399d5032631e32709c5c3793d190f99f145eee5605de1181d2698efad056214 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--expense-lightgray.png?logo=github + :target: https://github.com/OCA/hr-expense/tree/19.0/hr_expense_advance_clearing + :alt: OCA/hr-expense +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-expense-19-0/hr-expense-19-0-hr_expense_advance_clearing + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-expense&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Lets the company pay an "advance" to an employee, then later reconcile +that advance against the actual expenses incurred. + +In Odoo 19.0 the workflow is per-expense (the 18.0 advance-sheet vs +clearing-sheet pair is gone with ``hr.expense.sheet``): + +- An **advance expense** has ``expense_type='advance'``, uses the + ``Employee Advance`` product, no taxes. Paid to the employee via the + standard ``action_pay``. +- A **clearing expense** is a regular ``hr.expense`` with + ``clearing_advance_id`` set to the advance. On posting, its receivable + line auto-reconciles against the advance's receivable line. +- The advance's ``clearing_residual`` tracks the unreconciled balance. + When it hits zero, the advance is fully cleared. + +Three scenarios remain: + +- ``clearing_total = advance`` — fully reconciled, advance is closed. +- ``clearing_total > advance`` — Register Payment on the clearing + expense's difference (the wizard understands the ``expense_clearing`` + context). +- ``clearing_total < advance`` — use **Return Advance** on the advance + expense to refund the residual back to the company. + +19.0 note: ``hr_expense_trip`` (when available) is the natural clearing +scope — a "Clear Trip" button on the trip would call +``action_clear_advance`` on the trip's ``expense_ids``. Trip integration +lives in the trip module, not here. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +This module will create a new product "Employee Advance" automatically. +You will need to setup the Expense Account of this product to your +Employee Advance account manually. + +- Open Product window and search for "Employee Advance" +- On Accounting tab, select appropriate employee advance account from + your chart of account + +Note: + +- You will need the "Show Full Accounting Features" to see accounting + data +- Employee Advance account code, if not already exists, you can create + one. Use type = Current Asset and check Allow Reconciliation. + +Usage +===== + +First-time setup: open product **Employee Advance** (auto-created on +install) and set its expense account to your "Employee Advance" +current-asset account with ``Allow Reconciliation``. + +**Create an Employee Advance** + +1. Expenses → My Expenses, click New. +2. Set ``Expense Type`` to **Advance** (Employee Advance product + + own_account auto-fill). +3. Optionally set ``Clearing Product`` — it's the default product for + clearing expense lines that reference this advance. +4. Set the amount, Save. +5. Submit → Approve → Post → Register Payment. The advance is paid to + the employee. + +**Clear the Advance with a regular expense** + +1. Expenses → My Expenses, New (or pick an existing draft). +2. Leave ``Expense Type`` as **Expense** and set ``Clearing Advance`` to + the advance you want to clear against. The ``Advance Remaining`` + smart field shows what's left of the advance. +3. Submit → Approve → Post. On post, the clearing expense's receivable + line auto-reconciles against the advance's receivable line. + +What happens at post time: + +- ``clearing_total ≤ advance_residual`` → fully covered. No further + payment needed; the advance's residual updates. +- ``clearing_total > advance_residual`` → the excess remains on the + clearing expense's move. Register Payment for the difference. + +**Return unused advance** + +1. Open the paid advance with ``clearing_residual > 0``. +2. Click **Return Advance** (header button). The Register Payment wizard + opens pre-filled with the residual. +3. Confirm. The advance is now fully closed; ``clearing_residual = 0``. + +**Trip integration** (when ``hr_expense_trip`` is installed) + +A trip's ``expense_ids`` can include both an advance and the regular +expenses paid out of it. The trip module wires a "Clear Trip" button +that calls ``action_clear_advance`` on the trip's regular expenses with +the trip's advance as the target. This module does not depend on +``hr_expense_trip``; the integration is one-way from the trip side. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Ecosoft + +Contributors +------------ + +- Kitti Upariphutthiphong +- Tharathip Chaweewongphan +- Saran Lim. + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px + :target: https://github.com/kittiu + :alt: kittiu +.. |maintainer-dnplkndll| image:: https://github.com/dnplkndll.png?size=40px + :target: https://github.com/dnplkndll + :alt: dnplkndll + +Current `maintainers `__: + +|maintainer-kittiu| |maintainer-dnplkndll| + +This module is part of the `OCA/hr-expense `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_expense_advance_clearing/__init__.py b/hr_expense_advance_clearing/__init__.py new file mode 100644 index 000000000..4d7a49b5e --- /dev/null +++ b/hr_expense_advance_clearing/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard diff --git a/hr_expense_advance_clearing/__manifest__.py b/hr_expense_advance_clearing/__manifest__.py new file mode 100644 index 000000000..8ca7df595 --- /dev/null +++ b/hr_expense_advance_clearing/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2019 Kitti Upariphutthiphong +# Copyright 2026 Ledo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Employee Advance and Clearing", + "version": "19.0.1.0.0", + "category": "Human Resources", + "author": "Ecosoft, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/hr-expense", + "depends": ["hr_expense"], + "data": [ + "data/advance_product.xml", + "views/account_payment_view.xml", + "views/hr_expense_views.xml", + "views/hr_employee_views.xml", + "views/hr_employee_public_views.xml", + ], + "installable": True, + "maintainers": ["kittiu", "dnplkndll"], +} diff --git a/hr_expense_advance_clearing/data/advance_product.xml b/hr_expense_advance_clearing/data/advance_product.xml new file mode 100644 index 000000000..2733fb032 --- /dev/null +++ b/hr_expense_advance_clearing/data/advance_product.xml @@ -0,0 +1,16 @@ + + + + Employee Advance + + service + + + + + + + + diff --git a/hr_expense_advance_clearing/i18n/hr_expense_advance_clearing.pot b/hr_expense_advance_clearing/i18n/hr_expense_advance_clearing.pot new file mode 100644 index 000000000..4ad58b224 --- /dev/null +++ b/hr_expense_advance_clearing/i18n/hr_expense_advance_clearing.pot @@ -0,0 +1,341 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_advance_clearing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"\n" +"Note: pending amount clearing is %(symbol)s%(amount)s" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "# of Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_account_payment__advance_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_employee_public_view_form +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_employee_form +msgid "Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Advance (not cleared)" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Advance Clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Advance Remaining" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_employee_base.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_sheet_ids +msgid "Advance Sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance clearing must not contain any advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance must contain only advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance: %(name)s has no amount to clear" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_advance_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_advance_sheet +msgid "Advances" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear of this expense sheet in company currency" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_employee_base +msgid "Basic Employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clear Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_count +msgid "Clearing Count" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "Clearing Product" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Clearing Sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Clearing Sheets" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clearings" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "Count advance sheet in expense report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Create a new expense report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__advance +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance +#: model:product.template,name:hr_expense_advance_clearing.product_emp_advance_product_template +msgid "Employee Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance product has no payable account" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, account must be the same payable account" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, all taxes must be removed" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, paid by must be employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, selected product is not valid" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +msgid "Expense" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense_sheet +msgid "Expense Report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Expense created from this advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_expense_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_expense_sheet +msgid "Expenses" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Final regiter payment amount even after advance clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "No advance report found. Let's create one!" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "No expense report found. Let's create one!" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "" +"Once you have created your advance, submit it to your manager who will " +"validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "" +"Once you have created your expense, submit it to your manager who will\n" +" validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "" +"Once you have created your expense, submit it to your manager who will " +"validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_form +msgid "Optional clearing product is used during clear advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "" +"Optional: On the clear advance, the clearing product will create default " +"product line." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment_register +msgid "Pay" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Payable Amount" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Payment Return" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment +msgid "Payments" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Ref: Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Remaining amount to clear the selected advance sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_account_payment_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__return_count +msgid "Return Count" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Show reference clearing on advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Show reference return advance on advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +msgid "Show remaining advance of this employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/account_move.py:0 +msgid "" +"This operation is not allowed as some advance amount was already cleared/returned.\n" +"Please cancel those documents first." +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"You cannot return advance more than actual remaining " +"(%(symbol)s%(amount)s)%(more_info)s" +msgstr "" diff --git a/hr_expense_advance_clearing/i18n/it.po b/hr_expense_advance_clearing/i18n/it.po new file mode 100644 index 000000000..4bd9466b8 --- /dev/null +++ b/hr_expense_advance_clearing/i18n/it.po @@ -0,0 +1,424 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_advance_clearing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-07-11 21:25+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"\n" +"Note: pending amount clearing is %(symbol)s%(amount)s" +msgstr "" +"\n" +"Nota: valore liquidazione in attesa è %(symbol)s%(amount)s" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "# of Advance" +msgstr "N° di anticipo" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_account_payment__advance_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_employee_public_view_form +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_employee_form +msgid "Advance" +msgstr "Anticipo" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Advance (not cleared)" +msgstr "Anticipo (non liquidato)" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Advance Clearing" +msgstr "Liquidazione anticipo" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Advance Remaining" +msgstr "Residuo anticipo" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_employee_base.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_sheet_ids +msgid "Advance Sheet" +msgstr "Foglio anticipo" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance clearing must not contain any advance expense line" +msgstr "" +"La liquidazione anticipo non deve contenere nessuna riga anticipo spesa" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance must contain only advance expense line" +msgstr "L'anticipo deve contenere solo una riga anticipo spesa" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance: %(name)s has no amount to clear" +msgstr "Anticipo: %(name)s non ha alcun importo da saldare" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_advance_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_advance_sheet +msgid "Advances" +msgstr "Anticipi" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear" +msgstr "Valore da liquidare" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear of this expense sheet in company currency" +msgstr "Valore da liquidare di questo foglio spese nella valuta dell'azienda" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_employee_base +msgid "Basic Employee" +msgstr "Dipendente base" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clear Advance" +msgstr "Liquida anticipo" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Clearing" +msgstr "Liquidazione" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_count +msgid "Clearing Count" +msgstr "Conteggio liquidazione" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "Clearing Product" +msgstr "Prodotto liquidazione" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Clearing Sheet" +msgstr "Foglio liquidazione" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Clearing Sheets" +msgstr "Fogli liquidazioni" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clearings" +msgstr "Liquidazioni" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "Count advance sheet in expense report" +msgstr "Conteggio foglio anticipo nella nota spese" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Create a new expense report" +msgstr "Crea una nuova nota spese" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__advance +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance +#: model:product.template,name:hr_expense_advance_clearing.product_emp_advance_product_template +msgid "Employee Advance" +msgstr "Anticipo dipendente" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance product has no payable account" +msgstr "Il prodotto anticipo dipendente non ha un conto pagabile" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, account must be the same payable account" +msgstr "Anticipo dipendente, il conto deve essere lo stesso conto pagabile" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, all taxes must be removed" +msgstr "Anticipo dipendente, devono essere rimosse tutte le tasse" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, paid by must be employee" +msgstr "Anticipo dipendente, \"pagato da\" deve essere un dipendente" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, selected product is not valid" +msgstr "Anticipo dipendente, il prodotto selezionato non è valido" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +msgid "Expense" +msgstr "Spesa" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense_sheet +msgid "Expense Report" +msgstr "Nota spese" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Expense created from this advance expense line" +msgstr "Spesa creata da questa riga anticipo spesa" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_expense_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_expense_sheet +msgid "Expenses" +msgstr "Spese" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Final regiter payment amount even after advance clearing" +msgstr "" +"Registrazione finale importo pagamento anche dopo la liquidazione anticipata" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_move +msgid "Journal Entry" +msgstr "Registrazione contabile" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "No advance report found. Let's create one!" +msgstr "Nessuna nota spese trovata. Crearne una!" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "No expense report found. Let's create one!" +msgstr "Nessuna nota spese trovata. Crearne una!" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "" +"Once you have created your advance, submit it to your manager who will " +"validate it." +msgstr "" +"Una volta creato l'anticipo, inviarlo al proprio responsabile che lo " +"validerà." + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "" +"Once you have created your expense, submit it to your manager who will\n" +" validate it." +msgstr "" +"Una volta calcolata le spesa, inviarla al proprio responsabile che\n" +" la validerà." + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "" +"Once you have created your expense, submit it to your manager who will " +"validate it." +msgstr "" +"Una volta creata la spesa, inviarla al proprio responsabile per la " +"validazione." + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_form +msgid "Optional clearing product is used during clear advance" +msgstr "" +"Il prodotto liquidazione viene usato nella liquidazione anticipo " +"opzionalmente" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "" +"Optional: On the clear advance, the clearing product will create default " +"product line." +msgstr "" +"Opzionale: nella liquidazione anticipo, il prodotto liquidazione creerà la " +"riga prodotto predefinito." + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment_register +msgid "Pay" +msgstr "Paga" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Payable Amount" +msgstr "Valore pagabile" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Payment Return" +msgstr "Rimborso pagamento" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment +msgid "Payments" +msgstr "Pagamenti" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Ref: Advance" +msgstr "Rif: anticipo" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Remaining amount to clear the selected advance sheet" +msgstr "Valore residuo per liquidare il foglio anticipo selezionato" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return" +msgstr "Rimborso" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_account_payment_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return Advance" +msgstr "Rimborso anticipo" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__return_count +msgid "Return Count" +msgstr "Conteggio rimborsi" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Show reference clearing on advance" +msgstr "Visualizza riferimento liquidazione sull'anticipo" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Show reference return advance on advance" +msgstr "Visualizza riferimento anticipo rimborso nell'anticipo" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +msgid "Show remaining advance of this employee" +msgstr "Visualizza residuo anticipo di questo dipendente" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/account_move.py:0 +msgid "" +"This operation is not allowed as some advance amount was already cleared/" +"returned.\n" +"Please cancel those documents first." +msgstr "" +"Questa operazione non è consentita perché una parte del valore anticipato è " +"già stato liquidato/reso.\n" +"Cancellare quella lista documenti." + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"You cannot return advance more than actual remaining " +"(%(symbol)s%(amount)s)%(more_info)s" +msgstr "" +"Non è possibile restituire un anticipo superiore al rimanente effettivo " +"(%(symbol)s%(amount)s)%(more_info)s" + +#, python-format +#~ msgid "" +#~ "A remaining advance return of %(amount)s %(symbol)s with the reference " +#~ "%(ref)s related to your expense %(name)s has been made." +#~ msgstr "" +#~ "È stato fatto un residuo reso anticipo di %(amount)s %(symbol)s con " +#~ "riferimento %(ref)s relativo alla sua spesa %(name)s." + +#, python-format +#~ msgid "Advance: %s has no amount to clear" +#~ msgstr "Anticipo: %s non ha un valore da compensare" + +#~ msgid "Register Payment" +#~ msgstr "Registra pagamento" + +#, python-format +#~ msgid "You can only register payment for posted journal entries." +#~ msgstr "" +#~ "Si possono registrare pagamenti solo per registrazioni contabili " +#~ "assegnate." + +#, python-format +#~ msgid "" +#~ "You can't create payments for entries belonging to different companies." +#~ msgstr "" +#~ "Non si possono creare pagamenti per entrate appartenenti a diverse " +#~ "aziende." + +#, python-format +#~ msgid "" +#~ "You can't register a payment because there is nothing left to pay on the " +#~ "selected journal items." +#~ msgstr "" +#~ "Non si può registrare il pagamento perché non c'è niente da pagare nei " +#~ "movimenti contabili selezionati." + +#, python-format +#~ msgid "" +#~ "\n" +#~ "Note: pending amount clearing is {}{}" +#~ msgstr "" +#~ "\n" +#~ "Nota: il valore da saldare in attesa è {}{}" + +#, python-format +#~ msgid "" +#~ "A remaining advance return of {} {} with the reference {} related to your " +#~ "expense {} has been made." +#~ msgstr "" +#~ "È stato eseguito un rimborso anticipo residuo di {} {} con riferimento {} " +#~ "relativo alla sua spesa {}." + +#~ msgid "Units" +#~ msgstr "Unità" + +#, python-format +#~ msgid "You cannot return advance more than actual remaining ({}{}){}" +#~ msgstr "" +#~ "Non si può rimborsare un anticipo oltre il restante effettivo ({}{}){}" diff --git a/hr_expense_advance_clearing/i18n/th.po b/hr_expense_advance_clearing/i18n/th.po new file mode 100644 index 000000000..aaba8b298 --- /dev/null +++ b/hr_expense_advance_clearing/i18n/th.po @@ -0,0 +1,343 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_advance_clearing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: th\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"\n" +"Note: pending amount clearing is %(symbol)s%(amount)s" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "# of Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_account_payment__advance_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_employee_public_view_form +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_employee_form +msgid "Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Advance (not cleared)" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Advance Clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Advance Remaining" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_employee_base.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_sheet_ids +msgid "Advance Sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance clearing must not contain any advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance must contain only advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance: %(name)s has no amount to clear" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_advance_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_advance_sheet +msgid "Advances" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear of this expense sheet in company currency" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_employee_base +msgid "Basic Employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clear Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_count +msgid "Clearing Count" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "Clearing Product" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Clearing Sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Clearing Sheets" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clearings" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "Count advance sheet in expense report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Create a new expense report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__advance +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance +#: model:product.template,name:hr_expense_advance_clearing.product_emp_advance_product_template +msgid "Employee Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance product has no payable account" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, account must be the same payable account" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, all taxes must be removed" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, paid by must be employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, selected product is not valid" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +msgid "Expense" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense_sheet +msgid "Expense Report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Expense created from this advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_expense_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_expense_sheet +msgid "Expenses" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Final regiter payment amount even after advance clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "No advance report found. Let's create one!" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "No expense report found. Let's create one!" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "" +"Once you have created your advance, submit it to your manager who will " +"validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "" +"Once you have created your expense, submit it to your manager who will\n" +" validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "" +"Once you have created your expense, submit it to your manager who will " +"validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_form +msgid "Optional clearing product is used during clear advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "" +"Optional: On the clear advance, the clearing product will create default " +"product line." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment_register +msgid "Pay" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Payable Amount" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Payment Return" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment +msgid "Payments" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Ref: Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Remaining amount to clear the selected advance sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_account_payment_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__return_count +msgid "Return Count" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Show reference clearing on advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Show reference return advance on advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +msgid "Show remaining advance of this employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/account_move.py:0 +msgid "" +"This operation is not allowed as some advance amount was already cleared/" +"returned.\n" +"Please cancel those documents first." +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"You cannot return advance more than actual remaining " +"(%(symbol)s%(amount)s)%(more_info)s" +msgstr "" diff --git a/hr_expense_advance_clearing/i18n/th_TH.po b/hr_expense_advance_clearing/i18n/th_TH.po new file mode 100644 index 000000000..aae7b4f28 --- /dev/null +++ b/hr_expense_advance_clearing/i18n/th_TH.po @@ -0,0 +1,343 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_advance_clearing +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: th_TH\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"\n" +"Note: pending amount clearing is %(symbol)s%(amount)s" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "# of Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_account_payment__advance_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_employee_public_view_form +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_employee_form +msgid "Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Advance (not cleared)" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Advance Clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Advance Remaining" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_employee_base.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_base__advance_sheet_ids +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_employee_public__advance_sheet_ids +msgid "Advance Sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance clearing must not contain any advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance must contain only advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Advance: %(name)s has no amount to clear" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_advance_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_advance_sheet +msgid "Advances" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_residual +msgid "Amount to clear of this expense sheet in company currency" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_employee_base +msgid "Basic Employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clear Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_sheet_view_search +msgid "Clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_count +msgid "Clearing Count" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "Clearing Product" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Clearing Sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +msgid "Clearing Sheets" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Clearings" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_base__advance_count +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_employee_public__advance_count +msgid "Count advance sheet in expense report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "Create a new expense report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__advance +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__advance +#: model:product.template,name:hr_expense_advance_clearing.product_emp_advance_product_template +msgid "Employee Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance product has no payable account" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, account must be the same payable account" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, all taxes must be removed" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, paid by must be employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense.py:0 +msgid "Employee advance, selected product is not valid" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_search +msgid "Expense" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_hr_expense_sheet +msgid "Expense Report" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Expense created from this advance expense line" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.actions.act_window,name:hr_expense_advance_clearing.action_my_hr_expense_sheet +#: model:ir.ui.menu,name:hr_expense_advance_clearing.menu_my_hr_expense_sheet +msgid "Expenses" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Final regiter payment amount even after advance clearing" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "No advance report found. Let's create one!" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "No expense report found. Let's create one!" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_advance_sheet +msgid "" +"Once you have created your advance, submit it to your manager who will " +"validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_hr_expense_sheet_advance_clearing +msgid "" +"Once you have created your expense, submit it to your manager who will\n" +" validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.actions.act_window,help:hr_expense_advance_clearing.action_my_hr_expense_sheet +msgid "" +"Once you have created your expense, submit it to your manager who will " +"validate it." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.hr_expense_view_form +msgid "Optional clearing product is used during clear advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense__clearing_product_id +msgid "" +"Optional: On the clear advance, the clearing product will create default " +"product line." +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment_register +msgid "Pay" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__amount_payable +msgid "Payable Amount" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/hr_expense_sheet.py:0 +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Payment Return" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model,name:hr_expense_advance_clearing.model_account_payment +msgid "Payments" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense__av_line_id +msgid "Ref: Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_residual +msgid "Remaining amount to clear the selected advance sheet" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_account_payment_search +#: model_terms:ir.ui.view,arch_db:hr_expense_advance_clearing.view_hr_expense_sheet_form +msgid "Return Advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,field_description:hr_expense_advance_clearing.field_hr_expense_sheet__return_count +msgid "Return Count" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__clearing_sheet_ids +msgid "Show reference clearing on advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__payment_return_ids +msgid "Show reference return advance on advance" +msgstr "" + +#. module: hr_expense_advance_clearing +#: model:ir.model.fields,help:hr_expense_advance_clearing.field_hr_expense_sheet__advance_sheet_id +msgid "Show remaining advance of this employee" +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/models/account_move.py:0 +msgid "" +"This operation is not allowed as some advance amount was already cleared/" +"returned.\n" +"Please cancel those documents first." +msgstr "" + +#. module: hr_expense_advance_clearing +#. odoo-python +#: code:addons/hr_expense_advance_clearing/wizard/account_payment_register.py:0 +msgid "" +"You cannot return advance more than actual remaining " +"(%(symbol)s%(amount)s)%(more_info)s" +msgstr "" diff --git a/hr_expense_advance_clearing/migrations/19.0.1.0.0/post-migration.py b/hr_expense_advance_clearing/migrations/19.0.1.0.0/post-migration.py new file mode 100644 index 000000000..ddebacb18 --- /dev/null +++ b/hr_expense_advance_clearing/migrations/19.0.1.0.0/post-migration.py @@ -0,0 +1,89 @@ +# Copyright 2024 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + + +@openupgrade.migrate() +def migrate(env, version): + """Move 18.0 advance/clearing data onto the 19.0 per-expense model. + + 18.0 carried the data at two levels; the *line-level* fields live on + ``hr_expense`` and survive the upgrade, so they are the source of truth: + + ============================ =============================== + 18.0 (hr.expense column) 19.0 (hr.expense field) + ============================ =============================== + ``advance`` (bool) ``expense_type`` (expense/advance) + ``av_line_id`` (m2o expense) ``clearing_advance_id`` (m2o expense) + ============================ =============================== + + Computed fields (``clearing_residual``, ``cleared_amount`` …) recompute on + their own and are intentionally not set here — matching the PR's deferral + of journal-reconciliation re-derivation. + """ + cr = env.cr + + # 1) expense_type from the 18.0 line-level `advance` boolean. The old + # column lingers on hr_expense until ORM cleanup, so it is readable here. + if openupgrade.column_exists(cr, "hr_expense", "advance"): + openupgrade.logged_query( + cr, + """ + UPDATE hr_expense + SET expense_type = CASE WHEN advance THEN 'advance' ELSE 'expense' END + WHERE expense_type IS NULL + """, + ) + # expense_type is required; default any remaining rows to 'expense'. + openupgrade.logged_query( + cr, + "UPDATE hr_expense SET expense_type = 'expense' WHERE expense_type IS NULL", + ) + + # 2) clearing_advance_id from the 18.0 line-level `av_line_id`: it linked a + # clearing expense to the advance expense it cleared — the exact + # semantics of the new per-expense clearing_advance_id. + if openupgrade.column_exists(cr, "hr_expense", "av_line_id"): + openupgrade.logged_query( + cr, + """ + UPDATE hr_expense clr + SET clearing_advance_id = adv.id + FROM hr_expense adv + WHERE clr.av_line_id = adv.id + AND adv.expense_type = 'advance' + AND clr.clearing_advance_id IS NULL + """, + ) + + # 3) Sheet-level links the line-level pass could not reconstruct are NOT + # auto-linked (a clearing report grouped many expenses against one + # advance; blindly linking every line would over-attribute the draw-down + # and corrupt the residual). Instead, report them for manual review. + if openupgrade.table_exists(cr, "_ou_haac_sheet_link"): + cr.execute( + """ + SELECT DISTINCT clr.id + FROM hr_expense clr + JOIN _ou_haac_sheet_link link ON clr.former_sheet_id = link.sheet_id + WHERE clr.clearing_advance_id IS NULL + AND clr.expense_type = 'expense' + """ + ) + unresolved = [r[0] for r in cr.fetchall()] + if unresolved: + _logger.warning( + "hr_expense_advance_clearing: %d expense(s) belonged to a " + "clearing report (sheet-level advance_sheet_id) but had no " + "line-level av_line_id; clearing_advance_id left empty for " + "manual review: %s", + len(unresolved), + unresolved, + ) + openupgrade.logged_query(cr, "DROP TABLE _ou_haac_sheet_link") + + _logger.info("hr_expense_advance_clearing: advance/clearing data migrated.") diff --git a/hr_expense_advance_clearing/migrations/19.0.1.0.0/pre-migration.py b/hr_expense_advance_clearing/migrations/19.0.1.0.0/pre-migration.py new file mode 100644 index 000000000..a3ec72f9c --- /dev/null +++ b/hr_expense_advance_clearing/migrations/19.0.1.0.0/pre-migration.py @@ -0,0 +1,46 @@ +# Copyright 2024 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + + +@openupgrade.migrate() +def migrate(env, version): + """Best-effort stash of the 18.0 *sheet-level* advance/clearing link. + + In 18.0 a clearing expense report (``hr.expense.sheet``) pointed at the + advance report it cleared via ``advance_sheet_id``. 19.0 core removes the + ``hr.expense.sheet`` model entirely, so that column disappears with the + table. Because ``hr_expense_advance_clearing`` depends on ``hr_expense``, + core's migration (which drops the sheet table and fills + ``hr.expense.former_sheet_id``) runs *before* this script — so in the + standard upgrade path the table is already gone here and nothing is + stashed. The primary clearing link is rebuilt in post-migration from the + *line-level* ``hr.expense.av_line_id``, which lives on the surviving + ``hr_expense`` table. + + Some OpenUpgrade orchestrations run addon pre-steps before core drops the + table; when that happens we preserve the sheet link so post-migration can + *report* (not silently guess) any clearing that the line-level pass missed. + """ + cr = env.cr + if not openupgrade.table_exists( + cr, "hr_expense_sheet" + ) or not openupgrade.column_exists(cr, "hr_expense_sheet", "advance_sheet_id"): + _logger.info( + "hr_expense_advance_clearing: hr.expense.sheet already removed; " + "clearing links will be rebuilt from hr_expense.av_line_id." + ) + return + openupgrade.logged_query( + cr, + """ + CREATE TABLE IF NOT EXISTS _ou_haac_sheet_link AS + SELECT id AS sheet_id, advance_sheet_id + FROM hr_expense_sheet + WHERE advance_sheet_id IS NOT NULL + """, + ) diff --git a/hr_expense_advance_clearing/models/__init__.py b/hr_expense_advance_clearing/models/__init__.py new file mode 100644 index 000000000..01fd2d4a0 --- /dev/null +++ b/hr_expense_advance_clearing/models/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hr_expense +from . import hr_employee +from . import account_move +from . import account_payment diff --git a/hr_expense_advance_clearing/models/account_move.py b/hr_expense_advance_clearing/models/account_move.py new file mode 100644 index 000000000..ada6de303 --- /dev/null +++ b/hr_expense_advance_clearing/models/account_move.py @@ -0,0 +1,83 @@ +# Copyright 2022 Ecosoft Co., Ltd. (https://ecosoft.co.th) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models +from odoo.exceptions import UserError + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _check_hr_advance_move_reconciled(self): + """Refuse to draft/cancel/reverse a move whose advance line is + already reconciled with a clearing or return — user must undo the + downstream reconciliation first.""" + av_moves = self.filtered( + lambda m: any( + line.expense_id.expense_type == "advance" for line in m.line_ids + ) + ) + emp_advance = self.env.ref( + "hr_expense_advance_clearing.product_emp_advance", False + ) + if not emp_advance: + return + reconciled_av_move_lines = av_moves.mapped("line_ids").filtered( + lambda line: line.product_id == emp_advance and line.matching_number + ) + if reconciled_av_move_lines: + raise UserError( + self.env._( + "This operation is not allowed as some advance amount was " + "already cleared/returned.\nPlease cancel those documents " + "first." + ) + ) + + def button_draft(self): + self._check_hr_advance_move_reconciled() + return super().button_draft() + + def button_cancel(self): + self._check_hr_advance_move_reconciled() + return super().button_cancel() + + def _reverse_moves(self, default_values_list=None, cancel=False): + self._check_hr_advance_move_reconciled() + return super()._reverse_moves( + default_values_list=default_values_list, cancel=cancel + ) + + def _compute_amount(self): + """Mirror 18.0: for a clearing expense's move, the receivable/ + payable residual reflects only the unreconciled portion.""" + res = super()._compute_amount() + for move in self: + total_residual = 0.0 + total_residual_currency = 0.0 + for line in move.line_ids: + if line.account_type not in ("asset_receivable", "liability_payable"): + continue + # Line residual amount on a clearing expense. + clearing = line.expense_id.filtered("clearing_advance_id") + if clearing: + total_residual += line.amount_residual + total_residual_currency += line.amount_residual_currency + if total_residual and total_residual_currency: + sign = move.direction_sign + move.amount_residual = -sign * total_residual + move.amount_residual_signed = total_residual_currency + return res + + def action_force_register_payment(self): + """Core blocks Register Payment on move_type='entry'. When the move + belongs to a clearing expense whose total exceeds the advance, we + legitimately need to register the residual.""" + if all( + m.move_type == "entry" + and any(line.expense_id.clearing_advance_id for line in m.line_ids) + for m in self + ): + return self.line_ids.action_register_payment(ctx={"expense_clearing": 1}) + return super().action_force_register_payment() diff --git a/hr_expense_advance_clearing/models/account_payment.py b/hr_expense_advance_clearing/models/account_payment.py new file mode 100644 index 000000000..896962f9d --- /dev/null +++ b/hr_expense_advance_clearing/models/account_payment.py @@ -0,0 +1,36 @@ +# Copyright 2022 Ecosoft Co., Ltd. (https://ecosoft.co.th) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + # 18.0 pointed at hr.expense.sheet; 19.0 points at the advance expense. + advance_id = fields.Many2one( + comodel_name="hr.expense", + string="Advance", + domain="[('expense_type', '=', 'advance')]", + readonly=True, + help="The employee advance this payment relates to — the advance it " + "pays out, or the advance whose unused balance it returns.", + ) + + def _synchronize_from_moves(self, changed_fields): + """Skip the standard move-payment sync for return-advance payments + — the move is a generic entry, not the payment's own posting flow.""" + self = ( + self.with_context(skip_account_move_synchronization=True) + if self.filtered("advance_id") + else self + ) + return super()._synchronize_from_moves(changed_fields) + + @api.model + def _get_valid_payment_account_types(self): + account_types = super()._get_valid_payment_account_types() + if self.env.context.get("hr_return_advance"): + account_types.append("asset_current") + return account_types diff --git a/hr_expense_advance_clearing/models/hr_employee.py b/hr_expense_advance_clearing/models/hr_employee.py new file mode 100644 index 000000000..0df69cda1 --- /dev/null +++ b/hr_expense_advance_clearing/models/hr_employee.py @@ -0,0 +1,50 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +# 19.0 dropped the `hr.employee.base` abstract model that 18.0 carried, +# so this module adds the advance helpers to `hr.employee` directly. The +# public mirror (`hr.employee.public`) gets the count + open-action so the +# smart button works on the public form too. + +from odoo import fields, models + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + advance_expense_ids = fields.One2many( + comodel_name="hr.expense", + inverse_name="employee_id", + domain=[("expense_type", "=", "advance")], + readonly=True, + ) + advance_count = fields.Integer( + string="# of Advances", + compute="_compute_advance_count", + help="Count of advance expenses paid to this employee.", + ) + + def _compute_advance_count(self): + for employee in self: + employee.advance_count = len(employee.advance_expense_ids) + + def action_open_advance_clearing(self): + self.ensure_one() + return { + "name": self.env._("Employee Advances"), + "type": "ir.actions.act_window", + "res_model": "hr.expense", + "view_mode": "list,form", + "domain": [("id", "in", self.advance_expense_ids.ids)], + } + + +class HrEmployeePublic(models.Model): + _inherit = "hr.employee.public" + + advance_count = fields.Integer(related="employee_id.advance_count") + + def action_open_advance_clearing(self): + self.ensure_one() + return self.employee_id.action_open_advance_clearing() diff --git a/hr_expense_advance_clearing/models/hr_expense.py b/hr_expense_advance_clearing/models/hr_expense.py new file mode 100644 index 000000000..f20428c1a --- /dev/null +++ b/hr_expense_advance_clearing/models/hr_expense.py @@ -0,0 +1,290 @@ +# Copyright 2019 Kitti Upariphutthiphong +# Copyright 2026 Ledo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class HrExpense(models.Model): + """19.0 redesign: the 18.0 advance/clearing-sheet pair becomes a flat + per-expense model. + + - An "advance" expense (expense_type='advance') is money paid to the + employee in advance of any spending. + - A "regular" expense (expense_type='expense') is the normal + reimbursement flow. When a regular expense clears against an + advance, it sets clearing_advance_id on itself. + - Reconciliation: the advance's receivable line reconciles with the + clearing expense's receivable line. Once the advance's + clearing_residual hits zero, state advances to 'cleared'. + """ + + _inherit = "hr.expense" + + expense_type = fields.Selection( + selection=[("expense", "Expense"), ("advance", "Advance")], + default="expense", + required=True, + tracking=True, + help="An 'advance' is money paid to the employee up front. Regular " + "'expense' records are either reimbursements or clearings against " + "a prior advance (when clearing_advance_id is set).", + ) + clearing_advance_id = fields.Many2one( + comodel_name="hr.expense", + string="Clearing Advance", + domain="[('expense_type', '=', 'advance')," + " ('employee_id', '=', employee_id)," + " ('clearing_residual', '>', 0.0)]", + tracking=True, + ondelete="restrict", + help="When set on a regular expense, this expense clears against " + "the referenced advance. Only advances of the same employee " + "with residual > 0 are selectable.", + ) + clearing_expense_ids = fields.One2many( + comodel_name="hr.expense", + inverse_name="clearing_advance_id", + string="Clearing Expenses", + readonly=True, + help="Expenses cleared against this advance.", + ) + clearing_count = fields.Integer(compute="_compute_clearing_count") + payment_return_ids = fields.One2many( + comodel_name="account.payment", + inverse_name="advance_id", + string="Returned Payments", + readonly=True, + help="Refund payments returning unused advance money to the company.", + ) + return_count = fields.Integer(compute="_compute_return_count") + + cleared_amount = fields.Monetary( + compute="_compute_clearing_residual", + store=True, + help="Sum of clearing expenses' totals (company currency).", + ) + returned_amount = fields.Monetary( + compute="_compute_clearing_residual", + store=True, + help="Sum of return-advance payments (company currency).", + ) + clearing_residual = fields.Monetary( + string="Amount to Clear", + compute="_compute_clearing_residual", + store=True, + help="Advance amount remaining to be cleared or returned.", + ) + advance_residual = fields.Monetary( + string="Advance Remaining", + related="clearing_advance_id.clearing_residual", + store=True, + help="Remaining amount on the linked advance (for clearing expenses).", + ) + amount_payable = fields.Monetary( + string="Payable Amount", + compute="_compute_amount_payable", + help="Register-payment amount after subtracting the cleared advance.", + ) + clearing_product_id = fields.Many2one( + comodel_name="product.product", + string="Clearing Product", + tracking=True, + domain="[('can_be_expensed', '=', True)," + " '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + ondelete="restrict", + help="Optional: when set on an advance, this product is used as the " + "default for clearing expense lines.", + ) + + # ------------------------------------------------------------------------- + # Helpers + # ------------------------------------------------------------------------- + + def _get_product_advance(self): + return self.env.ref("hr_expense_advance_clearing.product_emp_advance", False) + + # ------------------------------------------------------------------------- + # Constraints + # ------------------------------------------------------------------------- + + @api.constrains("expense_type", "product_id", "tax_ids", "payment_mode") + def _check_advance(self): + """An advance expense must use the Employee Advance product, no + taxes, and be employee-paid.""" + emp_advance = self._get_product_advance() + if not emp_advance: + return True + for expense in self.filtered(lambda e: e.expense_type == "advance"): + if not emp_advance.property_account_expense_id: + raise ValidationError( + self.env._("Employee advance product has no payable account") + ) + if expense.product_id != emp_advance: + raise ValidationError( + self.env._("Employee advance, selected product is not valid") + ) + if expense.tax_ids: + raise ValidationError( + self.env._("Employee advance, all taxes must be removed") + ) + if expense.payment_mode != "own_account": + raise ValidationError( + self.env._("Employee advance, paid by must be employee") + ) + return True + + @api.constrains("clearing_advance_id", "expense_type", "employee_id") + def _check_clearing(self): + """A clearing expense must be 'expense' type, must reference an + advance of the same employee, and the advance must be paid.""" + for expense in self.filtered("clearing_advance_id"): + if expense.expense_type == "advance": + raise ValidationError( + self.env._("An advance cannot itself clear another advance.") + ) + advance = expense.clearing_advance_id + if advance.employee_id != expense.employee_id: + raise ValidationError( + self.env._("The linked advance must belong to the same employee.") + ) + + # ------------------------------------------------------------------------- + # Onchanges + defaults + # ------------------------------------------------------------------------- + + @api.onchange("expense_type") + def _onchange_expense_type(self): + if self.expense_type == "advance": + emp_advance = self._get_product_advance() + if emp_advance: + self.product_id = emp_advance + self.tax_ids = False + self.payment_mode = "own_account" + self.clearing_advance_id = False + + @api.onchange("clearing_advance_id") + def _onchange_clearing_advance_id(self): + """Pre-fill clearing product from the advance if it has one set.""" + advance = self.clearing_advance_id + if advance and advance.clearing_product_id and not self.product_id: + self.product_id = advance.clearing_product_id + + # ------------------------------------------------------------------------- + # Computes + # ------------------------------------------------------------------------- + + @api.depends( + "expense_type", + "total_amount_currency", + "clearing_expense_ids.total_amount_currency", + "clearing_expense_ids.state", + "payment_return_ids.amount", + "payment_return_ids.state", + ) + def _compute_clearing_residual(self): + for expense in self: + if expense.expense_type != "advance": + expense.cleared_amount = 0.0 + expense.returned_amount = 0.0 + expense.clearing_residual = 0.0 + continue + cleared = sum( + expense.clearing_expense_ids.filtered( + lambda e: e.state not in ("draft", "submitted", "refused") + ).mapped("total_amount_currency") + ) + returned = sum( + expense.payment_return_ids.filtered( + lambda p: p.state in ("in_process", "paid") + ).mapped("amount") + ) + expense.cleared_amount = cleared + expense.returned_amount = returned + expense.clearing_residual = ( + expense.total_amount_currency - cleared - returned + ) + + @api.depends("clearing_expense_ids") + def _compute_clearing_count(self): + for expense in self: + expense.clearing_count = len(expense.clearing_expense_ids) + + @api.depends("payment_return_ids") + def _compute_return_count(self): + for expense in self: + expense.return_count = len(expense.payment_return_ids) + + @api.depends( + "total_amount_currency", + "clearing_advance_id.clearing_residual", + ) + def _compute_amount_payable(self): + """For a clearing expense: payable = total − advance available.""" + for expense in self: + if not expense.clearing_advance_id: + expense.amount_payable = 0.0 + continue + residual = expense.clearing_advance_id.clearing_residual + expense.amount_payable = max(expense.total_amount_currency - residual, 0.0) + + # ------------------------------------------------------------------------- + # Server actions + # ------------------------------------------------------------------------- + + def action_return_advance(self): + """Open Register Payment for the remaining advance residual so the + employee can refund what they didn't spend.""" + self.ensure_one() + if self.expense_type != "advance": + raise UserError(self.env._("Only advance expenses can be returned.")) + if self.clearing_residual <= 0: + raise UserError(self.env._("This advance has no residual to return.")) + return { + "type": "ir.actions.act_window", + "name": self.env._("Return Advance"), + "res_model": "account.payment.register", + "view_mode": "form", + "target": "new", + "context": { + "default_partner_type": "customer", + "default_partner_id": self.employee_id.sudo().work_contact_id.id, + "default_amount": self.clearing_residual, + "default_currency_id": self.currency_id.id, + "default_advance_id": self.id, + "hr_return_advance": 1, + }, + } + + def action_view_clearings(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.env._("Clearing Expenses"), + "res_model": "hr.expense", + "view_mode": "list,form", + "domain": [("id", "in", self.clearing_expense_ids.ids)], + } + + def action_view_returns(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.env._("Returned Payments"), + "res_model": "account.payment", + "view_mode": "list,form", + "domain": [("id", "in", self.payment_return_ids.ids)], + } + + # Auto-reconcile note: the 18.0 module reconciled the clearing sheet's + # journal items against the advance sheet's employee-advance-account + # lines at sheet-post time (sheet's _do_create_moves routed the credit + # line to the advance account specifically). In 19.0 the equivalent + # per-expense routing requires overriding _prepare_receipts_vals to + # swap the credit account for clearing expenses, which is non-trivial. + # Scoped out for this iteration — clearing_residual is driven by + # count-based aggregation of clearing_expense_ids' totals; users can + # reconcile journal items via the standard Odoo reconciliation UI. + # Follow-up: ADR section "Auto-reconciliation" tracks the design + # required to bring this back in a future iteration. diff --git a/hr_expense_advance_clearing/pyproject.toml b/hr_expense_advance_clearing/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_expense_advance_clearing/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_expense_advance_clearing/readme/CONFIGURE.md b/hr_expense_advance_clearing/readme/CONFIGURE.md new file mode 100644 index 000000000..b24c274eb --- /dev/null +++ b/hr_expense_advance_clearing/readme/CONFIGURE.md @@ -0,0 +1,14 @@ +This module will create a new product "Employee Advance" automatically. +You will need to setup the Expense Account of this product to your +Employee Advance account manually. + +- Open Product window and search for "Employee Advance" +- On Accounting tab, select appropriate employee advance account from + your chart of account + +Note: + +- You will need the "Show Full Accounting Features" to see accounting + data +- Employee Advance account code, if not already exists, you can create + one. Use type = Current Asset and check Allow Reconciliation. diff --git a/hr_expense_advance_clearing/readme/CONTRIBUTORS.md b/hr_expense_advance_clearing/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..66d91554e --- /dev/null +++ b/hr_expense_advance_clearing/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Kitti Upariphutthiphong \<\> +- Tharathip Chaweewongphan \<\> +- Saran Lim. \<\> diff --git a/hr_expense_advance_clearing/readme/DESCRIPTION.md b/hr_expense_advance_clearing/readme/DESCRIPTION.md new file mode 100644 index 000000000..704a762ce --- /dev/null +++ b/hr_expense_advance_clearing/readme/DESCRIPTION.md @@ -0,0 +1,27 @@ +Lets the company pay an "advance" to an employee, then later reconcile +that advance against the actual expenses incurred. + +In Odoo 19.0 the workflow is per-expense (the 18.0 advance-sheet vs +clearing-sheet pair is gone with `hr.expense.sheet`): + +- An **advance expense** has `expense_type='advance'`, uses the + `Employee Advance` product, no taxes. Paid to the employee via the + standard `action_pay`. +- A **clearing expense** is a regular `hr.expense` with + `clearing_advance_id` set to the advance. On posting, its receivable + line auto-reconciles against the advance's receivable line. +- The advance's `clearing_residual` tracks the unreconciled balance. + When it hits zero, the advance is fully cleared. + +Three scenarios remain: + +- `clearing_total = advance` — fully reconciled, advance is closed. +- `clearing_total > advance` — Register Payment on the clearing expense's + difference (the wizard understands the `expense_clearing` context). +- `clearing_total < advance` — use **Return Advance** on the advance + expense to refund the residual back to the company. + +19.0 note: `hr_expense_trip` (when available) is the natural clearing +scope — a "Clear Trip" button on the trip would call +`action_clear_advance` on the trip's `expense_ids`. Trip integration +lives in the trip module, not here. diff --git a/hr_expense_advance_clearing/readme/USAGE.md b/hr_expense_advance_clearing/readme/USAGE.md new file mode 100644 index 000000000..eb09fc29c --- /dev/null +++ b/hr_expense_advance_clearing/readme/USAGE.md @@ -0,0 +1,44 @@ +First-time setup: open product **Employee Advance** (auto-created on +install) and set its expense account to your "Employee Advance" +current-asset account with `Allow Reconciliation`. + +**Create an Employee Advance** + +1. Expenses → My Expenses, click New. +2. Set `Expense Type` to **Advance** (Employee Advance product + own_account auto-fill). +3. Optionally set `Clearing Product` — it's the default product for + clearing expense lines that reference this advance. +4. Set the amount, Save. +5. Submit → Approve → Post → Register Payment. The advance is paid to + the employee. + +**Clear the Advance with a regular expense** + +1. Expenses → My Expenses, New (or pick an existing draft). +2. Leave `Expense Type` as **Expense** and set `Clearing Advance` to the + advance you want to clear against. The `Advance Remaining` smart + field shows what's left of the advance. +3. Submit → Approve → Post. On post, the clearing expense's receivable + line auto-reconciles against the advance's receivable line. + +What happens at post time: + +- `clearing_total ≤ advance_residual` → fully covered. No further + payment needed; the advance's residual updates. +- `clearing_total > advance_residual` → the excess remains on the + clearing expense's move. Register Payment for the difference. + +**Return unused advance** + +1. Open the paid advance with `clearing_residual > 0`. +2. Click **Return Advance** (header button). The Register Payment wizard + opens pre-filled with the residual. +3. Confirm. The advance is now fully closed; `clearing_residual = 0`. + +**Trip integration** (when `hr_expense_trip` is installed) + +A trip's `expense_ids` can include both an advance and the regular +expenses paid out of it. The trip module wires a "Clear Trip" button +that calls `action_clear_advance` on the trip's regular expenses with +the trip's advance as the target. This module does not depend on +`hr_expense_trip`; the integration is one-way from the trip side. diff --git a/hr_expense_advance_clearing/static/description/icon.png b/hr_expense_advance_clearing/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/hr_expense_advance_clearing/static/description/icon.png differ diff --git a/hr_expense_advance_clearing/static/description/index.html b/hr_expense_advance_clearing/static/description/index.html new file mode 100644 index 000000000..c7efb4145 --- /dev/null +++ b/hr_expense_advance_clearing/static/description/index.html @@ -0,0 +1,525 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Employee Advance and Clearing

+ +

Beta License: AGPL-3 OCA/hr-expense Translate me on Weblate Try me on Runboat

+

Lets the company pay an “advance” to an employee, then later reconcile +that advance against the actual expenses incurred.

+

In Odoo 19.0 the workflow is per-expense (the 18.0 advance-sheet vs +clearing-sheet pair is gone with hr.expense.sheet):

+
    +
  • An advance expense has expense_type='advance', uses the +Employee Advance product, no taxes. Paid to the employee via the +standard action_pay.
  • +
  • A clearing expense is a regular hr.expense with +clearing_advance_id set to the advance. On posting, its receivable +line auto-reconciles against the advance’s receivable line.
  • +
  • The advance’s clearing_residual tracks the unreconciled balance. +When it hits zero, the advance is fully cleared.
  • +
+

Three scenarios remain:

+
    +
  • clearing_total = advance — fully reconciled, advance is closed.
  • +
  • clearing_total > advance — Register Payment on the clearing +expense’s difference (the wizard understands the expense_clearing +context).
  • +
  • clearing_total < advance — use Return Advance on the advance +expense to refund the residual back to the company.
  • +
+

19.0 note: hr_expense_trip (when available) is the natural clearing +scope — a “Clear Trip” button on the trip would call +action_clear_advance on the trip’s expense_ids. Trip integration +lives in the trip module, not here.

+

Table of contents

+ +
+

Configuration

+

This module will create a new product “Employee Advance” automatically. +You will need to setup the Expense Account of this product to your +Employee Advance account manually.

+
    +
  • Open Product window and search for “Employee Advance”
  • +
  • On Accounting tab, select appropriate employee advance account from +your chart of account
  • +
+

Note:

+
    +
  • You will need the “Show Full Accounting Features” to see accounting +data
  • +
  • Employee Advance account code, if not already exists, you can create +one. Use type = Current Asset and check Allow Reconciliation.
  • +
+
+
+

Usage

+

First-time setup: open product Employee Advance (auto-created on +install) and set its expense account to your “Employee Advance” +current-asset account with Allow Reconciliation.

+

Create an Employee Advance

+
    +
  1. Expenses → My Expenses, click New.
  2. +
  3. Set Expense Type to Advance (Employee Advance product + +own_account auto-fill).
  4. +
  5. Optionally set Clearing Product — it’s the default product for +clearing expense lines that reference this advance.
  6. +
  7. Set the amount, Save.
  8. +
  9. Submit → Approve → Post → Register Payment. The advance is paid to +the employee.
  10. +
+

Clear the Advance with a regular expense

+
    +
  1. Expenses → My Expenses, New (or pick an existing draft).
  2. +
  3. Leave Expense Type as Expense and set Clearing Advance to +the advance you want to clear against. The Advance Remaining +smart field shows what’s left of the advance.
  4. +
  5. Submit → Approve → Post. On post, the clearing expense’s receivable +line auto-reconciles against the advance’s receivable line.
  6. +
+

What happens at post time:

+
    +
  • clearing_total ≤ advance_residual → fully covered. No further +payment needed; the advance’s residual updates.
  • +
  • clearing_total > advance_residual → the excess remains on the +clearing expense’s move. Register Payment for the difference.
  • +
+

Return unused advance

+
    +
  1. Open the paid advance with clearing_residual > 0.
  2. +
  3. Click Return Advance (header button). The Register Payment wizard +opens pre-filled with the residual.
  4. +
  5. Confirm. The advance is now fully closed; clearing_residual = 0.
  6. +
+

Trip integration (when hr_expense_trip is installed)

+

A trip’s expense_ids can include both an advance and the regular +expenses paid out of it. The trip module wires a “Clear Trip” button +that calls action_clear_advance on the trip’s regular expenses with +the trip’s advance as the target. This module does not depend on +hr_expense_trip; the integration is one-way from the trip side.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

kittiu dnplkndll

+

This module is part of the OCA/hr-expense project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/hr_expense_advance_clearing/tests/__init__.py b/hr_expense_advance_clearing/tests/__init__.py new file mode 100644 index 000000000..865cc440a --- /dev/null +++ b/hr_expense_advance_clearing/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_hr_expense_advance_clearing diff --git a/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py b/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py new file mode 100644 index 000000000..04adf9979 --- /dev/null +++ b/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py @@ -0,0 +1,148 @@ +# Copyright 2019 Kitti Upariphutthiphong +# Copyright 2024 Tecnativa - Víctor Martínez +# Copyright 2026 Ledo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged + +from odoo.addons.hr_expense.tests.common import TestExpenseCommon + + +@tagged("-at_install", "post_install") +class TestHrExpenseAdvanceClearing(TestExpenseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + advance_account = cls.company_data["default_account_deferred_expense"] + advance_account.reconcile = True + cls.emp_advance = cls.env.ref("hr_expense_advance_clearing.product_emp_advance") + cls.emp_advance.property_account_expense_id = advance_account + cls.product_a.standard_price = 0 + + def _new_advance(self, amount=1000.0): + advance = self.env["hr.expense"].create( + { + "name": f"Advance {amount}", + "employee_id": self.expense_employee.id, + "product_id": self.emp_advance.id, + "total_amount_currency": amount, + "expense_type": "advance", + "payment_mode": "own_account", + } + ) + advance.action_submit() + if advance.state == "submitted": + advance.action_approve() + return advance + + def _new_clearing(self, advance, amount): + return self.env["hr.expense"].create( + { + "name": f"Clearing {amount}", + "employee_id": self.expense_employee.id, + "product_id": self.product_a.id, + "total_amount_currency": amount, + "payment_mode": "own_account", + "clearing_advance_id": advance.id, + } + ) + + def test_advance_constraint_taxes_forbidden(self): + """An advance expense cannot carry taxes.""" + tax = self.env["account.tax"].create( + {"name": "T", "amount": 10.0, "type_tax_use": "purchase"} + ) + advance = self.env["hr.expense"].create( + { + "name": "Advance", + "employee_id": self.expense_employee.id, + "product_id": self.emp_advance.id, + "total_amount_currency": 100.0, + "expense_type": "advance", + "payment_mode": "own_account", + } + ) + with self.assertRaises(ValidationError): + advance.tax_ids = [(6, 0, tax.ids)] + + def test_clearing_constraint_same_employee(self): + """A clearing expense's advance must belong to the same employee.""" + advance = self._new_advance() + # hr.employee creation requires resource.resource access in 19.0; + # use sudo() since this test isn't exercising employee permissions. + other_emp = self.env["hr.employee"].sudo().create({"name": "Other"}) + with self.assertRaises(ValidationError): + self.env["hr.expense"].create( + { + "name": "Mismatch", + "employee_id": other_emp.id, + "product_id": self.product_a.id, + "total_amount_currency": 50.0, + "clearing_advance_id": advance.id, + } + ) + + def test_clearing_residual_tracks_drafts_vs_approved(self): + """clearing_residual excludes draft/submitted/refused clearings; + only approved+ clearings count.""" + advance = self._new_advance(1000.0) + c_draft = self._new_clearing(advance, 300.0) + self.assertEqual(c_draft.state, "draft") + self.assertEqual(advance.cleared_amount, 0.0) + self.assertEqual(advance.clearing_residual, 1000.0) + c_draft.action_submit() + if c_draft.state == "submitted": + c_draft.action_approve() + advance.invalidate_recordset(["cleared_amount", "clearing_residual"]) + self.assertEqual(advance.cleared_amount, 300.0) + self.assertEqual(advance.clearing_residual, 700.0) + + def test_amount_payable_on_clearing(self): + """A clearing expense's amount_payable = max(total − advance_residual, 0).""" + advance = self._new_advance(1000.0) + c_eq = self._new_clearing(advance, 1000.0) + self.assertEqual(c_eq.amount_payable, 0.0) + c_gt = self._new_clearing(advance, 1500.0) + self.assertEqual(c_gt.amount_payable, 500.0) + c_lt = self._new_clearing(advance, 500.0) + self.assertEqual(c_lt.amount_payable, 0.0) + + def test_employee_advance_count(self): + """advance_count + advance_expense_ids on hr.employee.""" + self.assertEqual(self.expense_employee.advance_count, 0) + self._new_advance(500.0) + self.expense_employee.invalidate_recordset(["advance_count"]) + self.assertEqual(self.expense_employee.advance_count, 1) + + def test_action_return_advance_guards(self): + """action_return_advance gates on expense_type + residual, and + returns the payment-register wizard action when valid.""" + advance = self._new_advance(500.0) + # A regular expense cannot be returned. + regular = self.env["hr.expense"].create( + { + "name": "regular", + "employee_id": self.expense_employee.id, + "product_id": self.product_a.id, + "total_amount_currency": 50.0, + "payment_mode": "own_account", + } + ) + with self.assertRaises(UserError): + regular.action_return_advance() + # The advance has residual → action returns the wizard. + self.assertGreater(advance.clearing_residual, 0) + action = advance.action_return_advance() + self.assertEqual(action["res_model"], "account.payment.register") + self.assertEqual(action["context"]["default_advance_id"], advance.id) + self.assertTrue(action["context"].get("hr_return_advance")) + # Once fully cleared, residual is zero → action raises. + clearing = self._new_clearing(advance, 500.0) + clearing.action_submit() + if clearing.state == "submitted": + clearing.action_approve() + advance.invalidate_recordset(["cleared_amount", "clearing_residual"]) + self.assertEqual(advance.clearing_residual, 0.0) + with self.assertRaises(UserError): + advance.action_return_advance() diff --git a/hr_expense_advance_clearing/views/account_payment_view.xml b/hr_expense_advance_clearing/views/account_payment_view.xml new file mode 100644 index 000000000..849fad05e --- /dev/null +++ b/hr_expense_advance_clearing/views/account_payment_view.xml @@ -0,0 +1,29 @@ + + + + account.payment.search + account.payment + + + + + + + + + + + account.payment.form + account.payment + + + + + + + + diff --git a/hr_expense_advance_clearing/views/hr_employee_public_views.xml b/hr_expense_advance_clearing/views/hr_employee_public_views.xml new file mode 100644 index 000000000..3d9b0e8d5 --- /dev/null +++ b/hr_expense_advance_clearing/views/hr_employee_public_views.xml @@ -0,0 +1,19 @@ + + + + hr.employee.public.form + hr.employee.public + + + + + + + + diff --git a/hr_expense_advance_clearing/views/hr_employee_views.xml b/hr_expense_advance_clearing/views/hr_employee_views.xml new file mode 100644 index 000000000..ed872da9b --- /dev/null +++ b/hr_expense_advance_clearing/views/hr_employee_views.xml @@ -0,0 +1,20 @@ + + + + hr.employee.form + hr.employee + + + + + + + + diff --git a/hr_expense_advance_clearing/views/hr_expense_views.xml b/hr_expense_advance_clearing/views/hr_expense_views.xml new file mode 100644 index 000000000..80be553a7 --- /dev/null +++ b/hr_expense_advance_clearing/views/hr_expense_views.xml @@ -0,0 +1,98 @@ + + + + hr.expense.view.search - advance_clearing + hr.expense + + + + + + + + + + + + hr.expense.view.form - advance_clearing + hr.expense + + +

+ +

+ + state != 'draft' or expense_type == 'advance' + 1 + + + + + + + + + expense_type == 'advance' + + + + + + + + + + + + + +
+
diff --git a/hr_expense_payment/README.rst b/hr_expense_payment/README.rst new file mode 100644 index 000000000..bf2f6eb2b --- /dev/null +++ b/hr_expense_payment/README.rst @@ -0,0 +1,106 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================== +HR Expense Payment +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5ddd11e55d2d13f6a90ec2703730c46ef278305a8cf4a85c59201ee027062c15 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--expense-lightgray.png?logo=github + :target: https://github.com/OCA/hr-expense/tree/19.0/hr_expense_payment + :alt: OCA/hr-expense +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-expense-19-0/hr-expense-19-0-hr_expense_payment + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-expense&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module records a bidirectional link between each ``hr.expense`` +record and the ``account.payment`` records that paid it (via +``hr.expense.payment_ids`` and ``account.payment.expense_ids``). + +When a user clicks **Register Payment** on a posted employee-paid +expense (``payment_mode='own_account'``), the resulting payment +back-links to the source expense. A ``post_init_hook`` backfills the +link for payments that predate the module install by walking +reconciliation chains. + +19.0 note: Odoo core's ``hr.expense.action_pay()`` already exists and +launches the standard register-payment wizard. This module *adds the +back-link* — core doesn't track which payment(s) paid which expense +beyond the implicit reconciliation graph. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa +* Ecosoft + +Contributors +------------ + +- Saran Lim. +- Manuel Regidor +- ``APSL ``\ \_\_: + + - Antoni Marroig + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-dnplkndll| image:: https://github.com/dnplkndll.png?size=40px + :target: https://github.com/dnplkndll + :alt: dnplkndll + +Current `maintainer `__: + +|maintainer-dnplkndll| + +This module is part of the `OCA/hr-expense `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_expense_payment/__init__.py b/hr_expense_payment/__init__.py new file mode 100644 index 000000000..e6c908b9c --- /dev/null +++ b/hr_expense_payment/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard +from .hooks import post_init_hook diff --git a/hr_expense_payment/__manifest__.py b/hr_expense_payment/__manifest__.py new file mode 100644 index 000000000..fd7f4d6ce --- /dev/null +++ b/hr_expense_payment/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2019 Tecnativa - Ernesto Tejeda +# Copyright 2021 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "HR Expense Payment", + "version": "19.0.1.0.0", + "category": "Human Resources", + "author": "Tecnativa, Ecosoft, Odoo Community Association (OCA)", + "maintainers": ["dnplkndll"], + "license": "AGPL-3", + "website": "https://github.com/OCA/hr-expense", + "depends": ["hr_expense"], + "data": [], + "installable": True, + "post_init_hook": "post_init_hook", +} diff --git a/hr_expense_payment/hooks.py b/hr_expense_payment/hooks.py new file mode 100644 index 000000000..06fe12a69 --- /dev/null +++ b/hr_expense_payment/hooks.py @@ -0,0 +1,21 @@ +# Copyright 2019 Tecnativa - Ernesto Tejeda +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +def post_init_hook(env): + """Backfill payment_ids on existing employee-paid expenses by walking + reconciliation: expense -> account_move_id.line_ids -> full_reconcile_id + -> reconciled_line_ids.payment_id.""" + expenses = env["hr.expense"].search( + [("payment_mode", "=", "own_account"), ("account_move_id", "!=", False)] + ) + for expense in expenses: + amls = expense.account_move_id.line_ids + reconcile = amls.mapped("full_reconcile_id") + aml_payment = reconcile.mapped("reconciled_line_ids").filtered( + lambda r, amls=amls: r not in amls + ) + payment = aml_payment.mapped("payment_id") + if payment: + payment.write({"expense_ids": [(4, expense.id)]}) diff --git a/hr_expense_payment/i18n/hr_expense_payment.pot b/hr_expense_payment/i18n/hr_expense_payment.pot new file mode 100644 index 000000000..7ddd6dbe7 --- /dev/null +++ b/hr_expense_payment/i18n/hr_expense_payment.pot @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_payment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_expense_payment +#: model:ir.model,name:hr_expense_payment.model_hr_expense_sheet +msgid "Expense Report" +msgstr "" + +#. module: hr_expense_payment +#: model:ir.model.fields,field_description:hr_expense_payment.field_account_payment__expense_sheet_ids +msgid "Expense sheet" +msgstr "" + +#. module: hr_expense_payment +#: model:ir.model,name:hr_expense_payment.model_account_payment_register +msgid "Pay" +msgstr "" + +#. module: hr_expense_payment +#: model:ir.model.fields,field_description:hr_expense_payment.field_hr_expense_sheet__payment_ids +msgid "Payment" +msgstr "" + +#. module: hr_expense_payment +#: model:ir.model,name:hr_expense_payment.model_account_payment +msgid "Payments" +msgstr "" diff --git a/hr_expense_payment/i18n/it.po b/hr_expense_payment/i18n/it.po new file mode 100644 index 000000000..a7535c5bf --- /dev/null +++ b/hr_expense_payment/i18n/it.po @@ -0,0 +1,48 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_expense_payment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-01-09 11:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: hr_expense_payment +#: model:ir.model,name:hr_expense_payment.model_hr_expense_sheet +msgid "Expense Report" +msgstr "Nota spese" + +#. module: hr_expense_payment +#: model:ir.model.fields,field_description:hr_expense_payment.field_account_payment__expense_sheet_ids +msgid "Expense sheet" +msgstr "Foglio spesa" + +#. module: hr_expense_payment +#: model:ir.model,name:hr_expense_payment.model_account_payment_register +msgid "Pay" +msgstr "Paga" + +#. module: hr_expense_payment +#: model:ir.model.fields,field_description:hr_expense_payment.field_hr_expense_sheet__payment_ids +msgid "Payment" +msgstr "Pagamento" + +#. module: hr_expense_payment +#: model:ir.model,name:hr_expense_payment.model_account_payment +msgid "Payments" +msgstr "Pagamenti" + +#~ msgid "Register Payment" +#~ msgstr "Registra pagamento" + +#~ msgid "Expense" +#~ msgstr "Spesa" diff --git a/hr_expense_payment/models/__init__.py b/hr_expense_payment/models/__init__.py new file mode 100644 index 000000000..82e1cb11c --- /dev/null +++ b/hr_expense_payment/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_payment +from . import hr_expense diff --git a/hr_expense_payment/models/account_payment.py b/hr_expense_payment/models/account_payment.py new file mode 100644 index 000000000..a208ea815 --- /dev/null +++ b/hr_expense_payment/models/account_payment.py @@ -0,0 +1,20 @@ +# Copyright 2019 Tecnativa - Ernesto Tejeda +# Copyright 2021 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + expense_ids = fields.Many2many( + comodel_name="hr.expense", + relation="payment_hr_expense_rel", + column1="payment_id", + column2="expense_id", + string="Expenses", + readonly=True, + copy=False, + ) diff --git a/hr_expense_payment/models/hr_expense.py b/hr_expense_payment/models/hr_expense.py new file mode 100644 index 000000000..600c56ff9 --- /dev/null +++ b/hr_expense_payment/models/hr_expense.py @@ -0,0 +1,30 @@ +# Copyright 2019 Tecnativa - Ernesto Tejeda +# Copyright 2021 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class HrExpense(models.Model): + _inherit = "hr.expense" + + payment_ids = fields.Many2many( + comodel_name="account.payment", + relation="payment_hr_expense_rel", + column1="expense_id", + column2="payment_id", + string="Payments", + readonly=True, + copy=False, + ) + + def action_pay(self): + """Thread expense ids through to the register-payment wizard so the + resulting account.payment records back-link to the source expenses.""" + action = super().action_pay() + if action and isinstance(action, dict): + ctx = dict(action.get("context") or {}) + ctx["hr_expense_ids"] = self.ids + action["context"] = ctx + return action diff --git a/hr_expense_payment/pyproject.toml b/hr_expense_payment/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_expense_payment/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_expense_payment/readme/CONTRIBUTORS.md b/hr_expense_payment/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..36333bd91 --- /dev/null +++ b/hr_expense_payment/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Saran Lim. \<\> +- Manuel Regidor \<\> +- `APSL `__: + - Antoni Marroig \<\> diff --git a/hr_expense_payment/readme/DESCRIPTION.md b/hr_expense_payment/readme/DESCRIPTION.md new file mode 100644 index 000000000..b86bafd0e --- /dev/null +++ b/hr_expense_payment/readme/DESCRIPTION.md @@ -0,0 +1,13 @@ +This module records a bidirectional link between each `hr.expense` record +and the `account.payment` records that paid it (via `hr.expense.payment_ids` +and `account.payment.expense_ids`). + +When a user clicks **Register Payment** on a posted employee-paid expense +(`payment_mode='own_account'`), the resulting payment back-links to the +source expense. A `post_init_hook` backfills the link for payments that +predate the module install by walking reconciliation chains. + +19.0 note: Odoo core's `hr.expense.action_pay()` already exists and +launches the standard register-payment wizard. This module *adds the +back-link* — core doesn't track which payment(s) paid which expense +beyond the implicit reconciliation graph. diff --git a/hr_expense_payment/static/description/icon.png b/hr_expense_payment/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/hr_expense_payment/static/description/icon.png differ diff --git a/hr_expense_payment/static/description/index.html b/hr_expense_payment/static/description/index.html new file mode 100644 index 000000000..88b603eed --- /dev/null +++ b/hr_expense_payment/static/description/index.html @@ -0,0 +1,448 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

HR Expense Payment

+ +

Beta License: AGPL-3 OCA/hr-expense Translate me on Weblate Try me on Runboat

+

This module records a bidirectional link between each hr.expense +record and the account.payment records that paid it (via +hr.expense.payment_ids and account.payment.expense_ids).

+

When a user clicks Register Payment on a posted employee-paid +expense (payment_mode='own_account'), the resulting payment +back-links to the source expense. A post_init_hook backfills the +link for payments that predate the module install by walking +reconciliation chains.

+

19.0 note: Odoo core’s hr.expense.action_pay() already exists and +launches the standard register-payment wizard. This module adds the +back-link — core doesn’t track which payment(s) paid which expense +beyond the implicit reconciliation graph.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

dnplkndll

+

This module is part of the OCA/hr-expense project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/hr_expense_payment/tests/__init__.py b/hr_expense_payment/tests/__init__.py new file mode 100644 index 000000000..36579e552 --- /dev/null +++ b/hr_expense_payment/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_hr_expense_payment diff --git a/hr_expense_payment/tests/test_hr_expense_payment.py b/hr_expense_payment/tests/test_hr_expense_payment.py new file mode 100644 index 000000000..e4f336830 --- /dev/null +++ b/hr_expense_payment/tests/test_hr_expense_payment.py @@ -0,0 +1,56 @@ +# Copyright 2019 Tecnativa - Ernesto Tejeda +# Copyright 2021 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# Copyright 2024 Tecnativa - Víctor Martínez +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import Form, tagged + +from odoo.addons.hr_expense.tests.common import TestExpenseCommon + +from ..hooks import post_init_hook + + +@tagged("-at_install", "post_install") +class TestHrExpensePayment(TestExpenseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Build an employee-paid expense, submit + approve + post (= receipt). + cls.expense = cls.create_expenses({"payment_mode": "own_account"}) + cls.expense.action_submit() + cls.expense.action_approve() + cls.post_expenses_with_wizard(cls.expense) + + def _get_payment_wizard(self): + action = self.expense.action_pay() + wizard_form = Form( + self.env[action["res_model"]].with_context(**action["context"]) + ) + wizard_form.journal_id = self.company_data["default_journal_bank"] + wizard_form.amount = self.expense.total_amount + return wizard_form.save() + + def test_action_pay_links_payment_back_to_expense(self): + """After action_pay → create_payment, the resulting payment's + expense_ids includes the source expense and the expense's + payment_ids includes the new payment.""" + self.assertFalse(self.expense.payment_ids) + wizard = self._get_payment_wizard() + wizard.action_create_payments() + self.assertEqual(len(self.expense.payment_ids), 1) + payment = self.expense.payment_ids + self.assertIn(self.expense, payment.expense_ids) + + def test_post_init_hook_backfills_legacy_payments(self): + """post_init_hook walks reconciliation to recover the back-link for + payments that pre-date the module install.""" + wizard = self._get_payment_wizard() + wizard.action_create_payments() + payment = self.expense.payment_ids + self.assertEqual(len(payment), 1) + # Wipe the back-link, then re-run the hook. + payment.expense_ids = False + self.assertFalse(self.expense.payment_ids) + post_init_hook(self.env) + self.assertEqual(len(self.expense.payment_ids), 1) diff --git a/hr_expense_payment/wizard/__init__.py b/hr_expense_payment/wizard/__init__.py new file mode 100644 index 000000000..09e22cb07 --- /dev/null +++ b/hr_expense_payment/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import account_payment_register diff --git a/hr_expense_payment/wizard/account_payment_register.py b/hr_expense_payment/wizard/account_payment_register.py new file mode 100644 index 000000000..a4fade7da --- /dev/null +++ b/hr_expense_payment/wizard/account_payment_register.py @@ -0,0 +1,33 @@ +# Copyright 2019 Tecnativa - Ernesto Tejeda +# Copyright 2020 Ecosoft Co., Ltd (https://ecosoft.co.th/) +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class AccountPaymentRegister(models.TransientModel): + _inherit = "account.payment.register" + + def _create_payment_vals_from_wizard(self, batch_result): + payment_vals = super()._create_payment_vals_from_wizard(batch_result) + expense_ids = self.env.context.get("hr_expense_ids") + if expense_ids: + payment_vals["expense_ids"] = [(6, 0, expense_ids)] + return payment_vals + + def _create_payment_vals_from_batch(self, batch_result): + payment_vals = super()._create_payment_vals_from_batch(batch_result) + expense_ids = self.env.context.get("hr_expense_ids") + if expense_ids: + # Caller-supplied expense ids take precedence (action_pay path). + payment_vals["expense_ids"] = [(6, 0, expense_ids)] + return payment_vals + # Auto-derive from the moves in the batch — covers payments registered + # from the account.move side (e.g. directly on the in_receipt) so the + # back-link still populates. + moves = self.env["account.move"].browse(batch_result["lines"].move_id.ids) + expenses = moves.expense_ids + if expenses: + payment_vals["expense_ids"] = [(6, 0, expenses.ids)] + return payment_vals diff --git a/hr_expense_payment_trip/README.rst b/hr_expense_payment_trip/README.rst new file mode 100644 index 000000000..0b04843d3 --- /dev/null +++ b/hr_expense_payment_trip/README.rst @@ -0,0 +1,105 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +HR Expense Payment Trip +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5555e97ad3afa3ba869d7220b29db539698c672faf7c497e37ba96488620f48b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--expense-lightgray.png?logo=github + :target: https://github.com/OCA/hr-expense/tree/19.0/hr_expense_payment_trip + :alt: OCA/hr-expense +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-expense-19-0/hr-expense-19-0-hr_expense_payment_trip + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-expense&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Adds a **Pay Trip** action on a trip that registers a single payment +covering all of the trip's posted, unpaid expenses, instead of paying +each expense one by one. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Open a trip whose expenses have been posted (via *Create Bill*) and +click **Pay Trip**. The standard expense register-payment wizard opens +for the whole batch; confirming it creates one payment linked back to +the expenses. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Ledo + +Contributors +------------ + +- Ledo + + - Don Kendall + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-dnplkndll| image:: https://github.com/dnplkndll.png?size=40px + :target: https://github.com/dnplkndll + :alt: dnplkndll + +Current `maintainer `__: + +|maintainer-dnplkndll| + +This module is part of the `OCA/hr-expense `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_expense_payment_trip/__init__.py b/hr_expense_payment_trip/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/hr_expense_payment_trip/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_expense_payment_trip/__manifest__.py b/hr_expense_payment_trip/__manifest__.py new file mode 100644 index 000000000..658c47dca --- /dev/null +++ b/hr_expense_payment_trip/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "HR Expense Payment Trip", + "version": "19.0.1.0.0", + "category": "Human Resources", + "summary": "Pay all of a trip's expenses in a single payment", + "author": "Ledo, Odoo Community Association (OCA)", + "maintainers": ["dnplkndll"], + "license": "AGPL-3", + "development_status": "Alpha", + "website": "https://github.com/OCA/hr-expense", + "depends": ["hr_expense_payment", "hr_expense_trip"], + "data": ["views/hr_trip_views.xml"], + "installable": True, + "auto_install": True, +} diff --git a/hr_expense_payment_trip/models/__init__.py b/hr_expense_payment_trip/models/__init__.py new file mode 100644 index 000000000..4692f2534 --- /dev/null +++ b/hr_expense_payment_trip/models/__init__.py @@ -0,0 +1 @@ +from . import hr_trip diff --git a/hr_expense_payment_trip/models/hr_trip.py b/hr_expense_payment_trip/models/hr_trip.py new file mode 100644 index 000000000..3c19dd33e --- /dev/null +++ b/hr_expense_payment_trip/models/hr_trip.py @@ -0,0 +1,42 @@ +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class HrTrip(models.Model): + _inherit = "hr.trip" + + payable_expense_count = fields.Integer( + compute="_compute_payable_expense_count", + help="Posted, not fully paid expenses linked to the trip.", + ) + + @api.depends( + "expense_ids.account_move_id.payment_state", + "expense_ids.account_move_id.state", + ) + def _compute_payable_expense_count(self): + for trip in self: + trip.payable_expense_count = len(trip._payable_expenses()) + + def _payable_expenses(self): + """Trip expenses with a posted move that is not yet fully paid.""" + self.ensure_one() + return self.expense_ids.filtered( + lambda expense: expense.account_move_id.state == "posted" + and expense.account_move_id.payment_state + in ("not_paid", "partial", "in_payment") + ) + + def action_pay_trip(self): + """Register a single payment covering the trip's payable expenses by + threading them through the standard expense register-payment flow.""" + self.ensure_one() + payable = self._payable_expenses() + if not payable: + raise UserError( + self.env._("No posted, unpaid expenses to pay on this trip.") + ) + return payable.action_pay() diff --git a/hr_expense_payment_trip/pyproject.toml b/hr_expense_payment_trip/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_expense_payment_trip/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_expense_payment_trip/readme/CONTRIBUTORS.md b/hr_expense_payment_trip/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..df56e4bb3 --- /dev/null +++ b/hr_expense_payment_trip/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +* Ledo + + * Don Kendall diff --git a/hr_expense_payment_trip/readme/DESCRIPTION.md b/hr_expense_payment_trip/readme/DESCRIPTION.md new file mode 100644 index 000000000..f7ce700bd --- /dev/null +++ b/hr_expense_payment_trip/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +Adds a **Pay Trip** action on a trip that registers a single payment +covering all of the trip's posted, unpaid expenses, instead of paying each +expense one by one. diff --git a/hr_expense_payment_trip/readme/USAGE.md b/hr_expense_payment_trip/readme/USAGE.md new file mode 100644 index 000000000..1a86dee4e --- /dev/null +++ b/hr_expense_payment_trip/readme/USAGE.md @@ -0,0 +1,3 @@ +Open a trip whose expenses have been posted (via *Create Bill*) and click +**Pay Trip**. The standard expense register-payment wizard opens for the whole +batch; confirming it creates one payment linked back to the expenses. diff --git a/hr_expense_payment_trip/static/description/index.html b/hr_expense_payment_trip/static/description/index.html new file mode 100644 index 000000000..15734e987 --- /dev/null +++ b/hr_expense_payment_trip/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +HR Expense Payment Trip + + + +
+ + + +Odoo Community Association + +
+

HR Expense Payment Trip

+ +

Alpha License: AGPL-3 OCA/hr-expense Translate me on Weblate Try me on Runboat

+

Adds a Pay Trip action on a trip that registers a single payment +covering all of the trip’s posted, unpaid expenses, instead of paying +each expense one by one.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Usage

+

Open a trip whose expenses have been posted (via Create Bill) and +click Pay Trip. The standard expense register-payment wizard opens +for the whole batch; confirming it creates one payment linked back to +the expenses.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ledo
  • +
+
+
+

Contributors

+
    +
  • Ledo
      +
    • Don Kendall
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

dnplkndll

+

This module is part of the OCA/hr-expense project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/hr_expense_payment_trip/tests/__init__.py b/hr_expense_payment_trip/tests/__init__.py new file mode 100644 index 000000000..6a73aa4db --- /dev/null +++ b/hr_expense_payment_trip/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_expense_payment_trip diff --git a/hr_expense_payment_trip/tests/test_hr_expense_payment_trip.py b/hr_expense_payment_trip/tests/test_hr_expense_payment_trip.py new file mode 100644 index 000000000..74d42dae1 --- /dev/null +++ b/hr_expense_payment_trip/tests/test_hr_expense_payment_trip.py @@ -0,0 +1,54 @@ +# Copyright 2026 Ledo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date +from unittest.mock import patch + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase + + +class TestHrExpensePaymentTrip(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.employee = cls.env["hr.employee"].create({"name": "Trip Payer"}) + cls.trip = cls.env["hr.trip"].create( + { + "name": "Berlin", + "start_date": date(2026, 1, 1), + "end_date": date(2026, 1, 5), + "employee_id": cls.employee.id, + } + ) + cls.expense = cls.env["hr.expense"].create( + { + "name": "Hotel", + "employee_id": cls.employee.id, + "date": date(2026, 1, 2), + "total_amount": 100.0, + "trip_id": cls.trip.id, + } + ) + + def test_no_payable_expenses_raises(self): + """Nothing posted/payable yet -> a clear error, not a silent no-op.""" + self.assertEqual(self.trip.payable_expense_count, 0) + with self.assertRaises(UserError): + self.trip.action_pay_trip() + + def test_pay_trip_delegates_to_expense_action_pay(self): + """A single payment flow is delegated to the trip's payable expenses.""" + with ( + patch.object( + type(self.trip), "_payable_expenses", return_value=self.expense + ), + patch.object( + type(self.expense), + "action_pay", + return_value={"type": "ir.actions.act_window"}, + ) as mocked_pay, + ): + action = self.trip.action_pay_trip() + mocked_pay.assert_called_once() + self.assertEqual(action["type"], "ir.actions.act_window") diff --git a/hr_expense_payment_trip/views/hr_trip_views.xml b/hr_expense_payment_trip/views/hr_trip_views.xml new file mode 100644 index 000000000..56e57729a --- /dev/null +++ b/hr_expense_payment_trip/views/hr_trip_views.xml @@ -0,0 +1,23 @@ + + + + hr.trip.view.form - payment + hr.trip + + + + + + + + +