Author SHA1 Message Date
tryton-sync 4e7d12a621 Merge remote-tracking branch 'upstream/8.0' into automation/upstream-8.0 2026-10-15 00:37:18 +00:00
Cédric Krier 3ea0c2a04e Set the default cache timeout to 300 seconds as stated in the documentation
Closes #15160
(grafted from eda9f5c7918eeaf17f97bc7c75c50dedef9964ae)

--HG--
branch : 8.0
2026-10-12 17:40:15 +02:00
Cédric Krier e35702038c Sum internal quantity to calculate the stock inventory reporting
The quantity is in the unit of the stock move which is not always the same.

Closes #15166
(grafted from fb72b8d1cd1d3181935e6aff4e92dc4853af9ca9)

--HG--
branch : 8.0
2026-10-13 18:53:13 +02:00
Cédric Krier 8237437e53 Convert only Number instances to number when evaluate domain for inversion
Closes #15159
(grafted from 88cc5493f11bb7dc50026f87e34b416812eae666)

--HG--
branch : 8.0
2026-10-12 12:42:56 +02:00
Cédric Krier 082564ceda Convert account move line check_modification into on_modification
The method is calling validate_move which test the value in the database but
check_modification is called before the update of the database for write.
But on_modification is always called after the database modification.

Closes #15157
(grafted from a4629adf43a5dbf915a7eb031ae01e5763004396)

--HG--
branch : 8.0
2026-10-09 23:28:16 +02:00
Cédric Krier a9448b7ec8 Cast currency rounding when comparing against sum of debit - credit for SQLite
The Decimal value is adapted as string for SQLite backend so without column
affinity to trigger conversion, SQLite compares REAL against TEXT. And for
SQLite's comparison ordering, numeric values sort before text values.

Closes #15158
(grafted from 5c67a5358f10ebc9192f680acdc9e61b976719fc)

--HG--
branch : 8.0
2026-10-09 23:25:45 +02:00
Nicolas Évrard 37c0dd9d7c Early exits cache synchronization call when initializing the Pool
As threads could be launched when clean_timeout is 0, preforking WSGI could
unfortunately fork while in the cache listening thread making the server
unresponsive.

Closes #15120
(grafted from 69857991edf1f6dd9d0b535f7c0abe861c2b0c7f)

--HG--
branch : 8.0
2026-09-23 10:41:33 +02:00
Nicolas Évrard 05af3a2056 Turn the assert on the threads after Pool initialization into an exception
People running with PYTHONOPTIMIZE set deserve to know that something is going
wrong
(grafted from 261e0c174ddf435fda240d13ae518cc961a47e8a)

--HG--
branch : 8.0
2026-09-23 10:50:27 +02:00
Cédric Krier fa125204a7 Allow to create any action from the UI
The action field must not be used to create the main action and the inherited
fields from the parent action must not be set a second time.

Closes #15149
(grafted from 08c5c82cd1e2cb19a8877b706803dd03481863eb)

--HG--
branch : 8.0
2026-10-05 18:21:13 +02:00
Cédric Krier ca1a608c13 Catch all SubprocessError and OSError when converting report
The subprocess run may raises one of those exceptions.

Closes #15141
(grafted from 2dfbdcb8e6969bbf8d2021dab131386974cc5515)

--HG--
branch : 8.0
2026-10-02 23:00:51 +02:00
Cédric Krier 24f805356d Test if parent is not null before testing classList
Closes #15152
(grafted from 45cf3086a1f63732e3298845a1fa183375ec76b4)

--HG--
branch : 8.0
2026-10-06 13:12:24 +02:00
Nicolas Évrard 407e6b9a7f Do not go further than the modal window when computing the scrollable ancestors
Closes #15143
(grafted from ba1fb062e5bc109884cb8af66afd4c4de9e52037)

--HG--
branch : 8.0
2026-10-02 16:07:28 +02:00
Cédric Krier 2ff8d421a5 Fallback to 0 when cost price is empty for product of production
Closes #15138
(grafted from aaa87c6471b33c02922121d6f8849d2759a4208f)

--HG--
branch : 8.0
2026-10-02 11:11:48 +02:00
Cédric Krier 3f72a62365 Support phantom input and output when setting moves to production
Closes #14908
(grafted from 38aba6163a892562f07fdfce1ac10901638ca8c7)

--HG--
branch : 8.0
2026-10-02 10:48:48 +02:00
Cédric Krier 07aaf4393a Validate stdnum code before compacting and formatting it
Closes #15017
(grafted from 7d7d0ccfb5ab052e3a83b8d33c581d41dcf9eb38)

--HG--
branch : 8.0
2026-09-30 18:30:11 +02:00
Cédric Krier 072cd3b91f Show price and currency fields on stock move when they are not required but still filled
This is to let the user clear the invalid fields from past moves.

Closes #14919
(grafted from d5ace112f8a9315a6740ef75c2bda4cc8b94b90e)

--HG--
branch : 8.0
2026-10-01 09:54:29 +02:00
Cédric Krier 175cfc21b5 Use total package weight and volume as shipment weight for PDP
The shipment weight must not be less than the sum of parcel weights.

Closes #15121
(grafted from ed5fc0ff2cf3a191335821306d91349285393bb7)

--HG--
branch : 8.0
2026-10-01 15:30:57 +02:00
Cédric Krier 830d337d15 Do not normalize Decimal value in XML document
Some third party expect to always have the trailing 0 in the format of currency
amount. So the amount coming from the database must already have the right
precision to format.

Closes #15085
(grafted from 0ace319afd0fdf51ae4d6df4f6e1a39fc9005525)

--HG--
branch : 8.0
2026-09-28 11:26:56 +02:00
Nicolas Évrard 5d93555c35 Compute the minimal width of a treeview on display as it's done when parsing
Closes #15094
(grafted from 38edbf96602b3d3cc7f7fcea94d5154086513120)

--HG--
branch : 8.0
2026-10-01 15:32:58 +02:00
31 changed files with 346 additions and 367 deletions
-4
View File
@@ -36,10 +36,6 @@ organization grants that token read-only cross-repository access to
## Translation workflow
The complete Weblate architecture, translator procedure, promotion process,
deployment instructions, troubleshooting, and recovery checklist are in
[`dominican-translation-infrastructure.md`](dominican-translation-infrastructure.md).
The Tryton server owns `es_DO` as a standalone language; its database record
does not declare `es_419` (or any other locale) as parent. Complete Dominican
translations live beside each package in its regular `locale/es_DO.po`
@@ -1,222 +0,0 @@
# Dominican Spanish translation infrastructure
This document describes the production workflow for maintaining Spanish for
the Dominican Republic (`es_DO`) in the customized Tryton repository. It is
both an operating manual and a recovery reference.
## Design goals
- `es_DO` is a standalone Tryton language. It does not inherit from `es_419`.
- Every maintained series has its own catalogs and translation branch.
- Weblate never writes directly to a release branch.
- Translation changes are reviewed and validated before promotion.
- No password, personal access token, or API key is stored in Git.
## Components and ownership
| System | Responsibility |
| --- | --- |
| Official Tryton repository | Upstream source of Tryton changes. |
| `tryton-do/tryton-mirror` | Read-only private mirror of upstream history. |
| `tryton-do/tryton` | Customized source, Dominican catalogs, CI, and documentation. |
| Gitea Actions | Validates catalogs, Python code, packages, and relevant tests. |
| Weblate | Browser translation editor, checks, glossary, and translation memory. |
| Gitea webhook | Tells Weblate that the source repository changed. |
The data flow is:
```text
official upstream
|
v
tryton-mirror ---- scheduled proposal ----> integration/8.0 ---- review ----> 8.0
^
|
Weblate <---- webhook ---- tryton repository |
| |
+---- translation/weblate-8.0 ---- PR ------+
```
The same isolated Weblate branch model is used for all maintained series. The
8.0 production series adds the two-stage `integration/8.0` promotion gate.
## Version mapping
| Tryton series | Weblate project | Weblate Git branch | Promotion target |
| --- | --- | --- | --- |
| 6.0 | `tryton-6-0-es-do` | `translation/weblate-6.0` | `6.0` |
| 7.0 | `tryton-7-0-es-do` | `translation/weblate-7.0` | `7.0` |
| 8.0 | `tryton-8-0-es-do` | `translation/weblate-8.0` | `integration/8.0`, then `8.0` |
| 8.2 | `tryton-8-2-es-do` | `translation/weblate-8.2` | `8.2` |
Project pages are available below:
- <https://translate.tryton.org.do/projects/tryton-6-0-es-do/>
- <https://translate.tryton.org.do/projects/tryton-7-0-es-do/>
- <https://translate.tryton.org.do/projects/tryton-8-0-es-do/>
- <https://translate.tryton.org.do/projects/tryton-8-2-es-do/>
Each project contains independent components for the server core, server
resources, SAO, and every Tryton module present in that series. Component
discovery on the server-core component detects module catalogs matching:
```text
modules/<module>/locale/es_DO.po
```
The manually managed catalogs are:
```text
trytond/trytond/ir/locale/es_DO.po
trytond/trytond/res/locale/es_DO.po
sao/locale/es_DO.po
```
Only `es_DO` is exposed by the component language filter. Weblate can reuse
good translations between versions through shared translation memory, but it
commits each version to its own branch and catalog.
## Translator workflow
1. Sign in to Weblate and open the project for the required Tryton series.
2. Select Spanish (Dominican Republic) and the relevant component.
3. Translate or improve a string. Preserve placeholders, XML/HTML markup,
keyboard accelerators, and plural forms exactly as Weblate indicates.
4. Prefer Dominican accounting, tax, commercial, and administrative terms.
Use the project glossary for terminology that must be consistent.
5. Resolve Weblate checks before marking work complete. Do not suppress a
failing check merely to increase the completion percentage.
6. Use suggestions when the translator is uncertain; a reviewer can accept or
replace them without losing the discussion.
Weblate groups pending edits and commits them after the configured delay. It
then pushes to `translation/weblate-X.Y`. A translation is not released merely
because it appears in Weblate; it must pass the Git review flow below.
## Maintainer promotion workflow
1. Confirm that the Weblate component has no repository error or lock.
2. In Weblate, commit pending changes and push the repository when an immediate
release is required. Otherwise allow the automatic commit interval to run.
3. Inspect the version's `translation/weblate-X.Y` branch in Gitea.
4. Open a pull request to the promotion target from the version mapping table.
5. Review terminology and confirm that the diff only contains expected PO
catalogs. Reject unrelated generated or source changes.
6. Require the PO validation and relevant package tests to pass.
7. Merge the pull request. For 8.0, open and review the second pull request from
`integration/8.0` to `8.0`.
8. Allow the Gitea push webhook to refresh Weblate. If necessary, run
**Manage > Repository maintenance > Update** for the affected component.
Never rebase or force-push a protected release or integration branch. If a
Weblate branch must be repaired, first commit or discard pending Weblate work,
then fast-forward the branch from its promotion target.
## Deploying catalog changes
Build or install the Tryton packages from the reviewed release branch. On each
database, activate Spanish (Dominican Republic) as a translatable language and
run:
```console
trytond-admin -d DATABASE --all --activate-dependencies
```
Restart the Tryton services after updating the installed package or container.
Users can then select Spanish (Dominican Republic) in their preferences.
`es_419` does not need to be active and is not a runtime dependency.
For container deployments, update the image tag or immutable digest only after
the new package build succeeds. Back up the database before a production
module update and keep the previous image available for rollback.
## Adding a Tryton series
1. Create the release branch and `translation/weblate-X.Y` branch from the
same reviewed commit.
2. Add `es_DO` to the core language data and add an `es_DO.po` catalog beside
every maintained core, resource, module, and SAO catalog.
3. Create a Weblate project with hooks and shared translation memory enabled.
4. Add the server-core and server-resources PO components.
5. Add the SAO PO component.
6. Add component discovery to server core with this regular expression:
```text
^modules/(?P<component>[^/]+)/locale/(?P<language>es_DO)\.po$
```
7. Restrict languages to `^es_DO$`, disable creation of arbitrary languages,
enable push-on-commit, and point push to the dedicated translation branch.
8. Verify that the component count equals modules plus core, resources, SAO,
and the automatically created glossary.
9. Add CI and branch protection before accepting the first translation PR.
## Webhook and credential model
The Gitea repository has one active push webhook targeting:
```text
https://translate.tryton.org.do/hooks/gitea/
```
One repository-level webhook covers all translation branches. Weblate uses its
own repository credential to fetch and push; Gitea Actions uses the short-lived
repository-scoped `GITEA_TOKEN`. Personal passwords and Weblate API tokens must
remain in the respective secret stores, never in workflow YAML, URLs, logs, or
documentation.
Rotate a credential immediately if it is exposed. After rotation, test a fetch
and a push on a translation branch, then confirm that the webhook still causes
Weblate to update.
## Troubleshooting
### Weblate shows an outdated source
Confirm the Gitea webhook delivery, then update the repository from Weblate.
Check that the component branch matches the version mapping and that there are
no pending changes blocking a pull.
### Weblate cannot push
Check the component repository error, repository write permission, and branch
name. Weblate must push to `translation/weblate-X.Y`, not to a protected release
branch. Do not weaken release-branch protection to work around this error.
### A component is locked
Read the recorded repository error first. Correct authentication, connectivity,
or a Git conflict, update the repository, and only then unlock it. Repeatedly
unlocking without correcting the cause can lose or duplicate work.
### A new module is missing
Verify that `modules/<module>/locale/es_DO.po` exists on the translation branch,
then run component discovery from the server-core component. A missing catalog
must be added in Git before Weblate can discover it.
### A PO check fails in CI
Run the repository validator locally:
```console
python -m pip install polib
python ci/validate_po.py
```
Fix duplicate messages, invalid syntax, plural forms, or malformed placeholders
in Weblate or on the translation branch. Do not bypass the validator.
## Recovery and audit checklist
- Export or back up the Weblate database and application data with the normal
infrastructure backup process; Git alone does not retain suggestions,
comments, permissions, or translation memory.
- Verify Weblate backups by restoring them in an isolated environment.
- Keep the Gitea repository as the source of truth for released PO files.
- Audit Weblate project access and repository credentials periodically.
- Check that all four translation branches still exist and are not ahead with
unreviewed changes.
- Confirm webhook delivery and one repository refresh after maintenance.
- Record the pull request and deployed image/package version for every release.
+9 -9
View File
@@ -355,9 +355,11 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
'''
pool = Pool()
MoveLine = pool.get('account.move.line')
Currency = pool.get('currency.currency')
line = MoveLine.__table__()
transaction = Transaction()
cursor = transaction.connection.cursor()
cast = Currency.rounding.sql_cast
for company, moves in groupby(moves, key=lambda m: m.company):
currency = company.currency
@@ -370,7 +372,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) < abs(currency.rounding))
currency.digits)) < cast(abs(currency.rounding)))
cursor.execute(*line.update(
[line.state],
['valid'],
@@ -382,7 +384,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) >= abs(currency.rounding))
currency.digits)) >= cast(abs(currency.rounding)))
cursor.execute(*line.update(
[line.state],
['draft'],
@@ -1444,16 +1446,13 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
}])
@classmethod
def check_modification(cls, mode, lines, values=None, external=False):
def on_modification(cls, mode, lines, field_names=None):
pool = Pool()
Move = pool.get('account.move')
super().check_modification(
mode, lines, values=values, external=external)
super().on_modification(mode, lines, field_names=field_names)
if (mode in {'create', 'delete'}
or (mode == 'write'
and values.keys() - cls._check_modify_exclude)):
if field_names is None or (field_names - cls._check_modify_exclude):
journal_period_done = set()
for line in lines:
if line.move.state == 'posted':
@@ -1474,7 +1473,8 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
'account.msg_delete_line_reconciled',
line=line.rec_name))
if (mode == 'write'
and values.keys() & cls._reconciliation_modify_disallow):
and field_names is not None
and field_names & cls._reconciliation_modify_disallow):
for line in lines:
if line.reconciliation:
raise AccessError(gettext(
+63
View File
@@ -767,6 +767,69 @@ class AccountTestCase(
]
move.save()
@with_transaction()
def test_move_validation(self):
"Test move validation"
pool = Pool()
Party = pool.get('party.party')
FiscalYear = pool.get('account.fiscalyear')
Journal = pool.get('account.journal')
Account = pool.get('account.account')
Move = pool.get('account.move')
Line = pool.get('account.move.line')
party = Party(name='Party')
party.save()
company = create_company()
with set_company(company):
fiscalyear = get_fiscalyear(company)
fiscalyear.save()
FiscalYear.create_period([fiscalyear])
period = fiscalyear.periods[0]
create_chart(company)
journal_revenue, = Journal.search([
('code', '=', 'REV'),
])
revenue, = Account.search([
('type.revenue', '=', True),
('closed', '=', False),
], limit=1)
receivable, = Account.search([
('type.receivable', '=', True),
('closed', '=', False),
], limit=1)
move = Move()
move.period = period
move.journal = journal_revenue
move.date = period.start_date
move.lines = [
Line(account=revenue, credit=Decimal(100)),
]
move.save()
line, = move.lines
self.assertEqual(line.state, 'draft')
move.lines += (
Line(account=receivable, debit=Decimal(100), party=party),
)
move.save()
self.assertEqual([l.state for l in move.lines], ['valid', 'valid'])
line = move.lines[-1]
line.credit = Decimal(50)
line.save()
self.assertEqual(line.state, 'draft')
line.credit = Decimal(100)
line.save()
self.assertEqual(line.state, 'valid')
@with_transaction()
def test_find_best_reconciliation(self):
"Test find best reconciliation"
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -45,7 +45,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -77,7 +77,7 @@ this repository contains the full copyright notices and license terms. -->
</PmtId>
<!-- PmtTpInf --> <!-- EPC only at payment information level -->
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- EqvtAmt -->
</Amt>
<!-- XchgRateInf -->
@@ -13,7 +13,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -26,7 +26,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -59,7 +59,7 @@ this repository contains the full copyright notices and license terms. -->
</PmtId>
<!-- PmtTpInf --> <!-- EPC only at payment information level -->
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- EqvtAmt -->
</Amt>
<!-- XchgRateInf -->
@@ -15,7 +15,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party, id=group.journal.sepa_payable_initiator_id, with_address=False)}
</InitgPty>
@@ -25,7 +25,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -52,7 +52,7 @@ this repository contains the full copyright notices and license terms. -->
</PmtId>
<!-- PmtTpInf --> <!-- EPC only at payment information level -->
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
</Amt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<!-- UltmtDbtr -->
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -45,7 +45,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -83,7 +83,7 @@ this repository contains the full copyright notices and license terms. -->
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<!-- PmtTpInf -->
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
@@ -13,7 +13,7 @@ this repository contains the full copyright notices and license terms. -->
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<!-- Authstn -->
<NbOfTxs>${len(group.payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<!-- PmtTpInf -->
<!-- ReqdColltnDt -->
<InitgPty>
@@ -26,7 +26,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -64,7 +64,7 @@ this repository contains the full copyright notices and license terms. -->
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<!-- PmtTpInf -->
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
@@ -15,7 +15,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party, id=group.journal.sepa_receivable_initiator_id, with_address=False)}
</InitgPty>
@@ -25,7 +25,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
@@ -56,7 +56,7 @@ this repository contains the full copyright notices and license terms. -->
<InstrId>${payment.sepa_instruction_id}</InstrId>
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
@@ -26,7 +26,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party)}
</InitgPty>
@@ -36,7 +36,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>TRF</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<!-- InstrPrty -->
<SvcLvl>
@@ -62,7 +62,7 @@ this repository contains the full copyright notices and license terms. -->
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<Amt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<!-- EqvtAmt -->
</Amt>
<!-- ChrgBr --> <!-- EPC only at payment information level -->
@@ -32,7 +32,7 @@ this repository contains the full copyright notices and license terms. -->
<MsgId>${group.sepa_message_id[:35]}</MsgId>
<CreDtTm>${datetime.datetime.now().replace(microsecond=0).isoformat()}</CreDtTm>
<NbOfTxs>${sum(len(payments) for _, payments in group.sepa_payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments), 'f')}</CtrlSum>
<InitgPty>
${PartyIdentification(group.sepa_initiating_party)}
</InitgPty>
@@ -42,7 +42,7 @@ this repository contains the full copyright notices and license terms. -->
<PmtMtd>DD</PmtMtd>
<BtchBookg>${'true' if group.journal.sepa_batch_booking else 'false'}</BtchBookg>
<NbOfTxs>${len(payments)}</NbOfTxs>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments), 'f')}</CtrlSum>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
@@ -73,7 +73,7 @@ this repository contains the full copyright notices and license terms. -->
<InstrId>${payment.sepa_instruction_id}</InstrId>
<EndToEndId>${payment.sepa_end_to_end_id}</EndToEndId>
</PmtId>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount, 'f')}</InstdAmt>
<DrctDbtTx>
<MndtRltdInf py:with="mandate = payment.sepa_mandate">
<MndtId>${mandate.identification}</MndtId>
+13 -13
View File
@@ -90,7 +90,7 @@ this repository contains the full copyright notices and license terms. -->
<py:otherwise>
<cac:PaymentTerms py:for="line in this.invoice.lines_to_pay">
<cbc:Note py:if="this.invoice.payment_term and this.invoice.payment_term.description">${this.invoice.payment_term.description}</cbc:Note>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:Amount>
<cbc:PaymentDueDate>${line.maturity_date}</cbc:PaymentDueDate>
</cac:PaymentTerms>
</py:otherwise>
@@ -101,16 +101,16 @@ this repository contains the full copyright notices and license terms. -->
<cbc:AllowanceChargeReasonCode py:if="reason_code">${reason_code}</cbc:AllowanceChargeReasonCode>
</py:with>
<cbc:AllowanceChargeReason>${allowance_charge.product.name}</cbc:AllowanceChargeReason>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount).normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount), 'f')}</cbc:Amount>
<cac:TaxCategory py:for="tax_line in allowance_charge.invoice_taxes">
${TaxCategory(tax_line.tax)}
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal py:for="group, lines, amount in this.taxes">
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-amount).normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-amount, 'f')}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.base).normalize(), 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.amount).normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.base, 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.amount, 'f')}</cbc:TaxAmount>
<cbc:Percent py:if="line.tax.type == 'percentage' and not (specification or '').startswith('peppol')">${format((line.tax.rate * 100).normalize(), 'f')}</cbc:Percent>
<cac:TaxCategory>
${TaxCategory(line.tax)}
@@ -118,19 +118,19 @@ this repository contains the full copyright notices and license terms. -->
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.invoice.untaxed_amount).normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.invoice.untaxed_amount).normalize(), 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-(this.invoice.untaxed_amount + this.invoice.tax_amount)).normalize(), 'f')}</cbc:TaxInclusiveAmount>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.invoice.untaxed_amount, 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.invoice.untaxed_amount, 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-(this.invoice.untaxed_amount + this.invoice.tax_amount), 'f')}</cbc:TaxInclusiveAmount>
<py:if test="this.invoice.total_amount != (this.invoice.untaxed_amount + this.invoice.tax_amount)">
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-(this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount)).normalize(), 'f')}</cbc:PayableRoundingAmount>
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-(this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount), 'f')}</cbc:PayableRoundingAmount>
</py:if>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.prepaid_amount).normalize(), 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-this.invoice.total_amount + this.prepaid_amount).normalize(), 'f')}</cbc:PayableAmount>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.prepaid_amount, 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-this.invoice.total_amount + this.prepaid_amount, 'f')}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:CreditNoteLine py:for="line_id, line in enumerate(this.lines, 1)">
<cbc:ID>${line_id}</cbc:ID>
<cbc:CreditedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(-Decimal(str(line.quantity)), 'f')}</cbc:CreditedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.amount).normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:CreditedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(-Decimal(str(line.quantity)).normalize(), 'f')}</cbc:CreditedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.amount, 'f')}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description py:if="line.description">${line.description}</cbc:Description>
<cbc:Name>${line.product.name if line.product else line.summary}</cbc:Name>
+13 -13
View File
@@ -90,7 +90,7 @@ this repository contains the full copyright notices and license terms. -->
<py:otherwise>
<cac:PaymentTerms py:for="line in this.invoice.lines_to_pay">
<cbc:Note py:if="this.invoice.payment_term and this.invoice.payment_term.description">${this.invoice.payment_term.description}</cbc:Note>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:Amount>
<cbc:PaymentDueDate>${line.maturity_date}</cbc:PaymentDueDate>
</cac:PaymentTerms>
</py:otherwise>
@@ -101,16 +101,16 @@ this repository contains the full copyright notices and license terms. -->
<cbc:AllowanceChargeReasonCode py:if="reason_code">${reason_code}</cbc:AllowanceChargeReasonCode>
</py:with>
<cbc:AllowanceChargeReason>${allowance_charge.product.name}</cbc:AllowanceChargeReason>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount).normalize(), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount), 'f')}</cbc:Amount>
<cac:TaxCategory py:for="tax_line in allowance_charge.invoice_taxes">
${TaxCategory(tax_line.tax)}
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal py:for="group, lines, amount in this.taxes">
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount.normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount, 'f')}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.base.normalize(), 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.base, 'f')}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:TaxAmount>
<cbc:Percent py:if="line.tax.type == 'percentage' and not (specification or '').startswith('peppol')">${format((line.tax.rate * 100).normalize(), 'f')}</cbc:Percent>
<cac:TaxCategory>
${TaxCategory(line.tax)}
@@ -118,19 +118,19 @@ this repository contains the full copyright notices and license terms. -->
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount.normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount.normalize(), 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount + this.invoice.tax_amount).normalize(), 'f')}</cbc:TaxInclusiveAmount>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount, 'f')}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount, 'f')}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount + this.invoice.tax_amount), 'f')}</cbc:TaxInclusiveAmount>
<py:if test="this.invoice.total_amount != (this.invoice.untaxed_amount + this.invoice.tax_amount)">
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount).normalize(), 'f')}</cbc:PayableRoundingAmount>
<cbc:PayableRoundingAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount - this.invoice.untaxed_amount - this.invoice.tax_amount), 'f')}</cbc:PayableRoundingAmount>
</py:if>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.prepaid_amount.normalize(), 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount - this.prepaid_amount).normalize(), 'f')}</cbc:PayableAmount>
<cbc:PrepaidAmount py:if="this.prepaid_amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.prepaid_amount, 'f')}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.total_amount - this.prepaid_amount, 'f')}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine py:for="line_id, line in enumerate(this.lines, 1)">
<cbc:ID>${line_id}</cbc:ID>
<cbc:InvoicedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(Decimal(str(line.quantity)), 'f')}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), 'f')}</cbc:LineExtensionAmount>
<cbc:InvoicedQuantity py:attrs="{'unitCode': line.unit.unece_code if line.unit and line.unit.unece_code else 'ZZ'}">${format(Decimal(str(line.quantity)).normalize(), 'f')}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, 'f')}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description py:if="line.description">${line.description}</cbc:Description>
<cbc:Name>${line.product.name if line.product else line.summary}</cbc:Name>
@@ -44,11 +44,11 @@ this repository contains the full copyright notices and license terms. -->
</py:def>
<py:def function="TradeTax(tax, amount=None, base=None)">
<ram:ApplicableTradeTax>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format((amount * this.type_sign).normalize(), 'f')}</ram:CalculatedAmount>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount * this.type_sign, 'f')}</ram:CalculatedAmount>
<ram:TypeCode py:if="tax.unece_code">${tax.unece_code}</ram:TypeCode>
<ram:ExemptionReasonCode py:if="hasattr(tax, 'vatex_code') and tax.vatex_code">${tax.vatex_code}</ram:ExemptionReasonCode>
<ram:ExemptionReason py:if="tax.legal_notice">${tax.legal_notice}</ram:ExemptionReason>
<ram:BasisAmount py:if="base">${format((base * this.type_sign).normalize(), 'f')}</ram:BasisAmount>
<ram:BasisAmount py:if="base">${format(base * this.type_sign, 'f')}</ram:BasisAmount>
<ram:CategoryCode py:if="tax.unece_category_code">${tax.unece_category_code}</ram:CategoryCode>
<ram:RateApplicablePercent py:if="tax.type == 'percentage'">${format((tax.rate * 100).normalize(), 'f')}</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
@@ -74,18 +74,18 @@ this repository contains the full copyright notices and license terms. -->
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.currency.round(line.unit_price).normalize(), 'f')}</ram:ChargeAmount>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.currency.round(line.unit_price), 'f')}</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity py:attrs="{'unitCode': line.unit.unece_code} if line.unit and line.unit.unece_code else {}">${format(abs(Decimal(str(line.quantity))), 'f')}</ram:BilledQuantity>
<ram:BilledQuantity py:attrs="{'unitCode': line.unit.unece_code} if line.unit and line.unit.unece_code else {}">${format(abs(Decimal(str(line.quantity)).normalize()), 'f')}</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<py:for each="tax in line.invoice_taxes">
${TradeTax(tax.tax)}
</py:for>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((line.amount * this.type_sign).normalize(), 'f')}</ram:LineTotalAmount>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount * this.type_sign, 'f')}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
@@ -158,14 +158,14 @@ this repository contains the full copyright notices and license terms. -->
<ram:DueDateDateTime>
${DateTime(line.maturity_date)}
</ram:DueDateDateTime>
<ram:PartialPaymentAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(((line.amount_second_currency or (line.debit - line.credit)) * this.type_sign).normalize(), 'f')}</ram:PartialPaymentAmount>
<ram:PartialPaymentAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((line.amount_second_currency or (line.debit - line.credit)) * this.type_sign, 'f')}</ram:PartialPaymentAmount>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount * this.type_sign).normalize(), 'f')}</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.untaxed_amount * this.type_sign).normalize(), 'f')}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.tax_amount * this.type_sign).normalize(), 'f')}</ram:TaxTotalAmount>
<ram:GrandTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.total_amount * this.type_sign).normalize(), 'f')}</ram:GrandTotalAmount>
<ram:DuePayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((this.invoice.amount_to_pay * this.type_sign).normalize(), 'f')}</ram:DuePayableAmount>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount * this.type_sign, 'f')}</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.untaxed_amount * this.type_sign, 'f')}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.tax_amount * this.type_sign, 'f')}</ram:TaxTotalAmount>
<ram:GrandTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.total_amount * this.type_sign, 'f')}</ram:GrandTotalAmount>
<ram:DuePayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.amount_to_pay * this.type_sign, 'f')}</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
+40 -31
View File
@@ -395,12 +395,15 @@ class Party(
else:
for country in eu_vat.guess_country(text):
if 'eu_vat' in identifier_types:
code = eu_vat.compact(country + text)
add('eu_vat', code)
code = country + text
if eu_vat.is_valid(code):
code = eu_vat.compact(code)
add('eu_vat', code)
elif f'{country}_vat' in identifier_types:
vat = get_cc_module(country, 'vat')
code = vat.compact(text)
add(f'{country}_vat', code)
if vat.is_valid(text):
code = vat.compact(text)
add(f'{country}_vat', code)
return result
@classmethod
@@ -913,18 +916,20 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
@fields.depends('type', 'code')
def on_change_with_code(self):
code = self.code
if self.type and '_' in self.type:
if code and self.type and '_' in self.type:
if module := get_cc_module(*self.type.split('_', 1)):
if hasattr(module, 'compact'):
try:
code = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'format'):
try:
code = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if (hasattr(module, 'is_valid')
and module.is_valid(code)):
if hasattr(module, 'compact'):
try:
code = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'format'):
try:
code = module.format(code)
except stdnum.exceptions.ValidationError:
pass
return code
def pre_validate(self):
@@ -955,16 +960,18 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
if ((type := values.get('type')) and '_' in type
and (code := values.get('code'))):
if module := get_cc_module(*type.split('_', 1)):
if hasattr(module, 'format'):
try:
values['code'] = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'compact'):
try:
values['code_compact'] = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if (hasattr(module, 'is_valid')
and module.is_valid(code)):
if hasattr(module, 'format'):
try:
values['code'] = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'compact'):
try:
values['code_compact'] = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
return values
def compute_fields(self, field_names=None):
@@ -974,12 +981,14 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
code_compact = getattr(self, 'code_compact', None)
if (type := getattr(self, 'type', None)) and '_' in type and code:
if module := get_cc_module(*type.split('_', 1)):
if hasattr(module, 'format'):
code = module.format(code)
if hasattr(module, 'compact'):
code_compact = module.compact(code)
else:
code_compact = code
if (hasattr(module, 'is_valid')
and module.is_valid(code)):
if hasattr(module, 'format'):
code = module.format(code)
if hasattr(module, 'compact'):
code_compact = module.compact(code)
else:
code_compact = code
if getattr(self, 'code', None) != code:
values['code'] = code
if getattr(self, 'code_compact', None) != code_compact:
+13 -11
View File
@@ -470,7 +470,7 @@ class Production(
for input_ in self.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price()
cost_price = input_.get_cost_price() or 0
cost += (Decimal(str(input_.internal_quantity)) * cost_price)
return round_price(cost)
@@ -491,7 +491,7 @@ class Production(
product = input_.product
quantity = Uom.compute_qty(
input_.unit, input_.quantity, product.default_uom)
cost += Decimal(str(quantity)) * product.cost_price
cost += Decimal(str(quantity)) * (product.cost_price or 0)
return cost
@dualmethod
@@ -516,18 +516,20 @@ class Production(
for input_ in production.bom.inputs:
quantity = input_.compute_quantity(factor)
product = input_.product
move = production._move(
'input', product, input_.unit, quantity)
move.planned_date = input_date
to_save.append(input_.prepare_move(production, move))
for line, quantity in input_.lines_for_quantity(quantity):
move = production._move(
'input', product, input_.unit, quantity)
move.planned_date = input_date
to_save.append(input_.prepare_move(production, move))
for output in production.bom.outputs:
quantity = output.compute_quantity(factor)
product = output.product
move = production._move(
'output', product, output.unit, quantity)
move.planned_date = output_date
to_save.append(output.prepare_move(production, move))
for line, quantity in output.lines_for_quantity(quantity):
move = production._move(
'output', product, output.unit, quantity)
move.planned_date = output_date
to_save.append(output.prepare_move(production, move))
Move.save(to_save)
@classmethod
@@ -564,7 +566,7 @@ class Production(
for input_ in production.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price()
cost_price = input_.get_cost_price() or 0
input_quantities[input_.product] += (
Decimal(str(input_.internal_quantity)))
input_costs[input_.product] += (
+9 -5
View File
@@ -322,8 +322,10 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('unit_price_required'),
'required': Bool(Eval('unit_price_required')),
'invisible': (
~Eval('unit_price_required', False)
& (Eval('unit_price', None) == None)), # noqa: E711
'required': Eval('unit_price_required', False),
'readonly': Eval('state') != 'draft',
})
unit_price_company = fields.Function(
@@ -346,7 +348,7 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('cost_price_required'),
'invisible': ~Eval('cost_price_required', False),
'required': (
(Eval('state') == 'done')
& Eval('cost_price_required', False)),
@@ -370,8 +372,10 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('unit_price_required'),
'required': Bool(Eval('unit_price_required')),
'invisible': (
~Eval('unit_price_required')
& (Eval('currency', None) == None)), # noqa: E711
'required': Eval('unit_price_required', False),
'readonly': Eval('state') != 'draft',
},
help="The currency in which the unit price is specified.")
+3 -3
View File
@@ -154,13 +154,13 @@ class _InventoryMixin:
output_clause = (
(from_location.left >= left) & (from_location.right <= right))
start_quantity = Sum(
move.quantity * Case((output_clause, -1), else_=1),
move.internal_quantity * Case((output_clause, -1), else_=1),
filter_=(move_date < from_date))
input_quantity = Sum(
move.quantity,
move.internal_quantity,
filter_=input_clause & (move_date >= from_date))
output_quantity = Sum(
move.quantity,
move.internal_quantity,
filter_=output_clause & (move_date >= from_date))
date_column = Greatest(move_date, from_date)
@@ -50,6 +50,8 @@ Get currency::
Create product::
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> box2, = unit.duplicate(default={'factor': 2, 'rate': .5})
>>> box10, = unit.duplicate(default={'factor': 10, 'rate': .1})
>>> template = ProductTemplate()
>>> template.name = 'Product'
@@ -96,8 +98,8 @@ Fill warehouse::
>>> move.product = product
>>> move.from_location = storage_loc
>>> move.to_location = customer_loc
>>> move.unit = unit
>>> move.quantity = 2
>>> move.unit = box2
>>> move.quantity = 1
>>> move.effective_date = dt.date(2025, 1, 10)
>>> move.unit_price = Decimal('20.0000')
>>> move.currency = currency
@@ -111,7 +113,8 @@ Forecast some moves::
>>> move.product = product
>>> move.from_location = supplier_loc
>>> move.to_location = storage_loc
>>> move.quantity = 10
>>> move.unit = box10
>>> move.quantity = 1
>>> move.effective_date = dt.date(2025, 1, 5)
>>> move.unit_price = Decimal('10.0000')
>>> move.currency = currency
+10 -2
View File
@@ -281,10 +281,18 @@ class CreateDPDShipping(Wizard):
cm3 = UoM(ModelData.get_id('product', 'uom_cubic_centimeter'))
kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
volume = sum(
UoM.compute_qty(
p.volume_uom, p.total_volume, shipment.volume_uom, round=False)
for p in shipment.root_packages)
volume = round(UoM.compute_qty(
shipment.volume_uom, shipment.volume, cm3, round=False))
shipment.volume_uom, volume, cm3, round=False))
weight = sum(
UoM.compute_qty(
p.weight_uom, p.total_weight, shipment.weight_uom, round=False)
for p in shipment.root_packages)
weight = round(UoM.compute_qty(
shipment.weight_uom, shipment.weight, kg, round=False), 2)
shipment.weight_uom, weight, kg, round=False), 2)
return {
'generalShipmentData': {
'identificationNumber': shipment.number,
@@ -136,7 +136,9 @@ Create Package Type::
... name='Box',
... length=10, length_uom=cm,
... height=8, height_uom=cm,
... width=5, width_uom=cm)
... width=5, width_uom=cm,
... packaging_weight=50,
... packaging_weight_uom=g)
>>> box.save()
Create a DPD Carrier and the related credential::
+11 -4
View File
@@ -89,13 +89,16 @@
function getScrollableAncestors(el) {
let ancestors = [];
let parent = el.parentElement;
while (parent && parent !== document.body) {
while (parent && (parent !== document.body ||
!parent.classList.contains('modal'))) {
if (isScrollable(parent)) {
ancestors.push(parent);
}
parent = parent.parentElement;
}
ancestors.push(window);
if (parent && !parent.classList.contains('modal')) {
ancestors.push(window);
}
return ancestors;
}
@@ -2978,9 +2981,13 @@
return ((a.isDate == b.isDate) &&
(a.isDateTime == b.isDateTime) &&
(a.valueOf() == b.valueOf()));
} else if ((a instanceof Number) || (b instanceof Number)) {
return (Number(a) === Number(b));
} else {
if (a instanceof Number) {
a = a.valueOf();
}
if (b instanceof Number) {
b = b.valueOf();
}
return (a === b);
}
};
+5 -3
View File
@@ -1102,11 +1102,13 @@
width = column.col.data('default-width');
}
if (width && width.endsWith('%')) {
width = parseInt(width.slice(0, -1), 10) / 100;
// Reverse the calculation from _set_column_width
let factor = 1;
if (column.attributes.expand) {
width /= parseInt(column.attributes.expand, 10);
factor += parseInt(column.attributes.expand, 10);
}
width = `${width}em`;
width = parseInt(width.slice(0, -1), 10);
width = `${width / 100 / factor}em`;
}
min_width.push(width);
column.col.sao_show();
+38
View File
@@ -2969,6 +2969,44 @@
var now = new Date();
[
[[['x', '=', 42]], {'x': 42}, true],
[[['x', '=', 42]], {'x': new Number(42)}, true],
[[['x', '=', 42]], {'x': 0}, false],
[[['x', '=', 42]], {'x': new Number(0)}, false],
[[['x', '=', 42]], {'x': null}, false],
[[['x', '=', new Number(42)]], {'x': 42}, true],
[[['x', '=', new Number(42)]], {'x': new Number(42)}, true],
[[['x', '=', new Number(42)]], {'x': 0}, false],
[[['x', '=', new Number(42)]], {'x': new Number(0)}, false],
[[['x', '=', new Number(42)]], {'x': null}, false],
[[['x', '=', 0]], {'x': 0}, true],
[[['x', '=', 0]], {'x': new Number(0)}, true],
[[['x', '=', 0]], {'x': null}, false],
[[['x', '=', new Number(0)]], {'x': 0}, true],
[[['x', '=', new Number(0)]], {'x': new Number(0)}, true],
[[['x', '=', new Number(0)]], {'x': null}, false],
[[['x', '=', null]], {'x': 0}, false],
[[['x', '=', null]], {'x': new Number(0)}, false],
[[['x', '=', null]], {'x': null}, true],
[[['x', '!=', 42]], {'x': 42}, false],
[[['x', '!=', 42]], {'x': new Number(42)}, false],
[[['x', '!=', 42]], {'x': 0}, true],
[[['x', '!=', 42]], {'x': new Number(0)}, true],
[[['x', '!=', 42]], {'x': null}, true],
[[['x', '!=', new Number(42)]], {'x': 42}, false],
[[['x', '!=', new Number(42)]], {'x': new Number(42)}, false],
[[['x', '!=', new Number(42)]], {'x': 0}, true],
[[['x', '!=', new Number(42)]], {'x': new Number(0)}, true],
[[['x', '!=', new Number(42)]], {'x': null}, true],
[[['x', '!=', 0]], {'x': 0}, false],
[[['x', '!=', 0]], {'x': new Number(0)}, false],
[[['x', '!=', 0]], {'x': null}, true],
[[['x', '!=', new Number(0)]], {'x': 0}, false],
[[['x', '!=', new Number(0)]], {'x': new Number(0)}, false],
[[['x', '!=', new Number(0)]], {'x': null}, true],
[[['x', '!=', null]], {'x': 0}, true],
[[['x', '!=', null]], {'x': new Number(0)}, true],
[[['x', '!=', null]], {'x': null}, false],
[[['x', '>', 5]], {'x': 6}, true],
[[['x', '>', 5]], {'x': 4}, false],
[[['x', '>', null]], {'x': today}, false],
+4 -1
View File
@@ -46,4 +46,7 @@ if db_names:
for thread in threads:
thread.join()
assert len(threads := threading.enumerate()) == 1, f"len({threads}) != 1"
if len(threads := threading.enumerate()) != 1:
raise RuntimeError(
f"There are {len(threads)} threads running after initializing"
" the Pool")
+3 -1
View File
@@ -256,6 +256,8 @@ class MemoryCache(BaseCache):
@classmethod
def sync(cls, transaction):
if not Pool().ready:
return
if cls._clean_last is None:
cls._clean_last = dt.datetime.now()
return
@@ -298,7 +300,7 @@ class MemoryCache(BaseCache):
cls._clean_last = dt.datetime.now()
def sync_since(self, value):
return self._clean_last > value
return self._clean_last is not None and self._clean_last > value
@classmethod
def commit(cls, transaction):
+1 -1
View File
@@ -104,7 +104,7 @@ class TrytonConfigParser(configparser.ConfigParser):
self.add_section('bus')
self.set('bus', 'allow_subscribe', 'False')
self.set('bus', 'long_polling_timeout', str(5 * 60))
self.set('bus', 'cache_timeout', '5')
self.set('bus', 'cache_timeout', str(5 * 60))
self.set('bus', 'select_timeout', '5')
self.add_section('report')
self.add_section('html')
+9 -4
View File
@@ -345,7 +345,11 @@ class ActionMixin(ModelSQL):
action = fields.Many2One(
'ir.action', "Action",
required=True, readonly=True, ondelete='CASCADE')
required=True, ondelete='CASCADE',
states={
'required': Eval('id', -1) >= 0,
'readonly': Eval('id', -1) >= 0,
})
@classmethod
def __setup__(cls):
@@ -456,8 +460,9 @@ class ActionMixin(ModelSQL):
values = values.copy()
for field in values:
if field in Action._fields:
action_values[field] = values[field]
if hasattr(getattr(cls, field), 'set'):
if field != 'action':
action_values[field] = values[field]
elif hasattr(getattr(cls, field), 'set'):
later[field] = values[field]
for field in later:
del values[field]
@@ -468,7 +473,7 @@ class ActionMixin(ModelSQL):
if database.nextid(transaction.connection, cls._table):
database.setnextid(transaction.connection, cls._table,
database.currid(transaction.connection, Action._table))
if 'action' not in values:
if not values.get('action'):
action, = Action.create([action_values])
values['action'] = action.id
else:
+1 -1
View File
@@ -464,7 +464,7 @@ class Report(URLMixin, PoolBase):
time.sleep(0.02 * (retry - count))
try:
subprocess.run(args, timeout=timeout, check=True)
except subprocess.CalledProcessError:
except (subprocess.SubprocessError, OSError):
if count:
continue
logger.error(
+57
View File
@@ -884,6 +884,63 @@ class DomainInversionTestCase(TestCase):
unique_value(domain, single_value=False), (False, None, None))
def test_evaldomain(self):
domain = [['x', '=', 42]]
self.assertTrue(eval_domain(domain, {'x': 42}))
self.assertTrue(eval_domain(domain, {'x': Decimal('42')}))
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', Decimal('42')]]
self.assertTrue(eval_domain(domain, {'x': 42}))
self.assertTrue(eval_domain(domain, {'x': Decimal('42')}))
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', 0]]
self.assertTrue(eval_domain(domain, {'x': 0}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', Decimal('0')]]
self.assertTrue(eval_domain(domain, {'x': 0}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '=', None]]
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', 42]]
self.assertFalse(eval_domain(domain, {'x': 42}))
self.assertFalse(eval_domain(domain, {'x': Decimal('42')}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', Decimal('42')]]
self.assertFalse(eval_domain(domain, {'x': 42}))
self.assertFalse(eval_domain(domain, {'x': Decimal('42')}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', 0]]
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', Decimal('0')]]
self.assertFalse(eval_domain(domain, {'x': 0}))
self.assertFalse(eval_domain(domain, {'x': Decimal('0')}))
self.assertTrue(eval_domain(domain, {'x': None}))
domain = [['x', '!=', None]]
self.assertTrue(eval_domain(domain, {'x': 0}))
self.assertTrue(eval_domain(domain, {'x': Decimal('0')}))
self.assertFalse(eval_domain(domain, {'x': None}))
domain = [['x', '>', 5]]
self.assertTrue(eval_domain(domain, {'x': 6}))
self.assertFalse(eval_domain(domain, {'x': 4}))