Author SHA1 Message Date
José Arturo García 94ec414089 docs(i18n): document Weblate translation operations
Validate Tryton customizations / validate (push) Skipped
Validate Tryton customizations / validate (pull_request) Successful in 3m47s
2026-10-13 21:26:53 -04:00
joseagrc 612821e363 Promote standalone Dominican Spanish to Tryton 8.0
Validate Tryton customizations / validate (push) Successful in 3m47s
Promote the reviewed es_DO core, module and SAO catalogs from integration/8.0 after successful CI.
2026-10-13 20:49:23 -04:00
31 changed files with 367 additions and 346 deletions
+4
View File
@@ -36,6 +36,10 @@ 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`
@@ -0,0 +1,222 @@
# 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,11 +355,9 @@ 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
@@ -372,7 +370,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) < cast(abs(currency.rounding)))
currency.digits)) < abs(currency.rounding))
cursor.execute(*line.update(
[line.state],
['valid'],
@@ -384,7 +382,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
group_by=line.move,
having=Abs(Round(
Sum(line.debit - line.credit),
currency.digits)) >= cast(abs(currency.rounding)))
currency.digits)) >= abs(currency.rounding))
cursor.execute(*line.update(
[line.state],
['draft'],
@@ -1446,13 +1444,16 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
}])
@classmethod
def on_modification(cls, mode, lines, field_names=None):
def check_modification(cls, mode, lines, values=None, external=False):
pool = Pool()
Move = pool.get('account.move')
super().on_modification(mode, lines, field_names=field_names)
super().check_modification(
mode, lines, values=values, external=external)
if field_names is None or (field_names - cls._check_modify_exclude):
if (mode in {'create', 'delete'}
or (mode == 'write'
and values.keys() - cls._check_modify_exclude)):
journal_period_done = set()
for line in lines:
if line.move.state == 'posted':
@@ -1473,8 +1474,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
'account.msg_delete_line_reconciled',
line=line.rec_name))
if (mode == 'write'
and field_names is not None
and field_names & cls._reconciliation_modify_disallow):
and values.keys() & cls._reconciliation_modify_disallow):
for line in lines:
if line.reconciliation:
raise AccessError(gettext(
-63
View File
@@ -767,69 +767,6 @@ 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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in group.payments).normalize(), '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), 'f')}</CtrlSum>
<CtrlSum>${format(sum(p.amount for p in payments).normalize(), '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, 'f')}</InstdAmt>
<InstdAmt py:attrs="{'Ccy': payment.currency.code}">${format(payment.amount.normalize(), '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, 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), '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), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount).normalize(), '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, 'f')}</cbc:TaxAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-amount).normalize(), 'f')}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
<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: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: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, '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>
<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>
<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), '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)).normalize(), 'f')}</cbc:PayableRoundingAmount>
</py:if>
<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>
<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>
</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)).normalize(), 'f')}</cbc:CreditedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(-line.amount, '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)), 'f')}</cbc:CreditedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((-line.amount).normalize(), '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, 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), '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), 'f')}</cbc:Amount>
<cbc:Amount py:attrs="{'currencyID': this.invoice.currency.code}">${format(abs(allowance_charge.amount).normalize(), '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, 'f')}</cbc:TaxAmount>
<cbc:TaxAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(amount.normalize(), 'f')}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
<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: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: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, '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>
<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>
<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), '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).normalize(), 'f')}</cbc:PayableRoundingAmount>
</py:if>
<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>
<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>
</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)).normalize(), 'f')}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount, '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)), 'f')}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(line.amount.normalize(), '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, 'f')}</ram:CalculatedAmount>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${format((amount * this.type_sign).normalize(), '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, 'f')}</ram:BasisAmount>
<ram:BasisAmount py:if="base">${format((base * this.type_sign).normalize(), '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), 'f')}</ram:ChargeAmount>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format(this.invoice.currency.round(line.unit_price).normalize(), '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)).normalize()), '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))), '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, 'f')}</ram:LineTotalAmount>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${format((line.amount * this.type_sign).normalize(), '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, '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).normalize(), 'f')}</ram:PartialPaymentAmount>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<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: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:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
+31 -40
View File
@@ -395,15 +395,12 @@ class Party(
else:
for country in eu_vat.guess_country(text):
if 'eu_vat' in identifier_types:
code = country + text
if eu_vat.is_valid(code):
code = eu_vat.compact(code)
add('eu_vat', code)
code = eu_vat.compact(country + text)
add('eu_vat', code)
elif f'{country}_vat' in identifier_types:
vat = get_cc_module(country, 'vat')
if vat.is_valid(text):
code = vat.compact(text)
add(f'{country}_vat', code)
code = vat.compact(text)
add(f'{country}_vat', code)
return result
@classmethod
@@ -916,20 +913,18 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
@fields.depends('type', 'code')
def on_change_with_code(self):
code = self.code
if code and self.type and '_' in self.type:
if self.type and '_' in self.type:
if module := get_cc_module(*self.type.split('_', 1)):
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
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):
@@ -960,18 +955,16 @@ 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, '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
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):
@@ -981,14 +974,12 @@ 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, '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 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:
+11 -13
View File
@@ -470,7 +470,7 @@ class Production(
for input_ in self.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price() or 0
cost_price = input_.get_cost_price()
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 or 0)
cost += Decimal(str(quantity)) * product.cost_price
return cost
@dualmethod
@@ -516,20 +516,18 @@ class Production(
for input_ in production.bom.inputs:
quantity = input_.compute_quantity(factor)
product = input_.product
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))
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
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 = 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
@@ -566,7 +564,7 @@ class Production(
for input_ in production.inputs:
if input_.state == 'cancelled':
continue
cost_price = input_.get_cost_price() or 0
cost_price = input_.get_cost_price()
input_quantities[input_.product] += (
Decimal(str(input_.internal_quantity)))
input_costs[input_.product] += (
+5 -9
View File
@@ -322,10 +322,8 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': (
~Eval('unit_price_required', False)
& (Eval('unit_price', None) == None)), # noqa: E711
'required': Eval('unit_price_required', False),
'invisible': ~Eval('unit_price_required'),
'required': Bool(Eval('unit_price_required')),
'readonly': Eval('state') != 'draft',
})
unit_price_company = fields.Function(
@@ -348,7 +346,7 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': ~Eval('cost_price_required', False),
'invisible': ~Eval('cost_price_required'),
'required': (
(Eval('state') == 'done')
& Eval('cost_price_required', False)),
@@ -372,10 +370,8 @@ class Move(Workflow, ModelSQL, ModelView):
()),
],
states={
'invisible': (
~Eval('unit_price_required')
& (Eval('currency', None) == None)), # noqa: E711
'required': Eval('unit_price_required', False),
'invisible': ~Eval('unit_price_required'),
'required': Bool(Eval('unit_price_required')),
'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.internal_quantity * Case((output_clause, -1), else_=1),
move.quantity * Case((output_clause, -1), else_=1),
filter_=(move_date < from_date))
input_quantity = Sum(
move.internal_quantity,
move.quantity,
filter_=input_clause & (move_date >= from_date))
output_quantity = Sum(
move.internal_quantity,
move.quantity,
filter_=output_clause & (move_date >= from_date))
date_column = Greatest(move_date, from_date)
@@ -50,8 +50,6 @@ 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'
@@ -98,8 +96,8 @@ Fill warehouse::
>>> move.product = product
>>> move.from_location = storage_loc
>>> move.to_location = customer_loc
>>> move.unit = box2
>>> move.quantity = 1
>>> move.unit = unit
>>> move.quantity = 2
>>> move.effective_date = dt.date(2025, 1, 10)
>>> move.unit_price = Decimal('20.0000')
>>> move.currency = currency
@@ -113,8 +111,7 @@ Forecast some moves::
>>> move.product = product
>>> move.from_location = supplier_loc
>>> move.to_location = storage_loc
>>> move.unit = box10
>>> move.quantity = 1
>>> move.quantity = 10
>>> move.effective_date = dt.date(2025, 1, 5)
>>> move.unit_price = Decimal('10.0000')
>>> move.currency = currency
+2 -10
View File
@@ -281,18 +281,10 @@ 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, 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)
shipment.volume_uom, shipment.volume, cm3, round=False))
weight = round(UoM.compute_qty(
shipment.weight_uom, weight, kg, round=False), 2)
shipment.weight_uom, shipment.weight, kg, round=False), 2)
return {
'generalShipmentData': {
'identificationNumber': shipment.number,
@@ -136,9 +136,7 @@ Create Package Type::
... name='Box',
... length=10, length_uom=cm,
... height=8, height_uom=cm,
... width=5, width_uom=cm,
... packaging_weight=50,
... packaging_weight_uom=g)
... width=5, width_uom=cm)
>>> box.save()
Create a DPD Carrier and the related credential::
+4 -11
View File
@@ -89,16 +89,13 @@
function getScrollableAncestors(el) {
let ancestors = [];
let parent = el.parentElement;
while (parent && (parent !== document.body ||
!parent.classList.contains('modal'))) {
while (parent && parent !== document.body) {
if (isScrollable(parent)) {
ancestors.push(parent);
}
parent = parent.parentElement;
}
if (parent && !parent.classList.contains('modal')) {
ancestors.push(window);
}
ancestors.push(window);
return ancestors;
}
@@ -2981,13 +2978,9 @@
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);
}
};
+3 -5
View File
@@ -1102,13 +1102,11 @@
width = column.col.data('default-width');
}
if (width && width.endsWith('%')) {
// Reverse the calculation from _set_column_width
let factor = 1;
width = parseInt(width.slice(0, -1), 10) / 100;
if (column.attributes.expand) {
factor += parseInt(column.attributes.expand, 10);
width /= parseInt(column.attributes.expand, 10);
}
width = parseInt(width.slice(0, -1), 10);
width = `${width / 100 / factor}em`;
width = `${width}em`;
}
min_width.push(width);
column.col.sao_show();
-38
View File
@@ -2969,44 +2969,6 @@
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],
+1 -4
View File
@@ -46,7 +46,4 @@ if db_names:
for thread in threads:
thread.join()
if len(threads := threading.enumerate()) != 1:
raise RuntimeError(
f"There are {len(threads)} threads running after initializing"
" the Pool")
assert len(threads := threading.enumerate()) == 1, f"len({threads}) != 1"
+1 -3
View File
@@ -256,8 +256,6 @@ 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
@@ -300,7 +298,7 @@ class MemoryCache(BaseCache):
cls._clean_last = dt.datetime.now()
def sync_since(self, value):
return self._clean_last is not None and self._clean_last > value
return 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', str(5 * 60))
self.set('bus', 'cache_timeout', '5')
self.set('bus', 'select_timeout', '5')
self.add_section('report')
self.add_section('html')
+4 -9
View File
@@ -345,11 +345,7 @@ class ActionMixin(ModelSQL):
action = fields.Many2One(
'ir.action', "Action",
required=True, ondelete='CASCADE',
states={
'required': Eval('id', -1) >= 0,
'readonly': Eval('id', -1) >= 0,
})
required=True, readonly=True, ondelete='CASCADE')
@classmethod
def __setup__(cls):
@@ -460,9 +456,8 @@ class ActionMixin(ModelSQL):
values = values.copy()
for field in values:
if field in Action._fields:
if field != 'action':
action_values[field] = values[field]
elif hasattr(getattr(cls, field), 'set'):
action_values[field] = values[field]
if hasattr(getattr(cls, field), 'set'):
later[field] = values[field]
for field in later:
del values[field]
@@ -473,7 +468,7 @@ class ActionMixin(ModelSQL):
if database.nextid(transaction.connection, cls._table):
database.setnextid(transaction.connection, cls._table,
database.currid(transaction.connection, Action._table))
if not values.get('action'):
if 'action' not in values:
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.SubprocessError, OSError):
except subprocess.CalledProcessError:
if count:
continue
logger.error(
-57
View File
@@ -884,63 +884,6 @@ 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}))