Prepare NCF manager for production on Tryton 8.0
Publicar paquete PyPI / publish (push) Successful in 1m17s
Publicar paquete PyPI / publish (push) Successful in 1m17s
This commit is contained in:
+30
-160
@@ -1,192 +1,62 @@
|
||||
name: Publish Package
|
||||
name: Publicar paquete PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
branches: ["8.0"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
github-server-url: http://unpaismejor-gitea-app:3000
|
||||
ref: 8.0
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect package branch
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import re
|
||||
|
||||
branch = (
|
||||
os.environ.get('GITHUB_REF_NAME')
|
||||
or os.environ.get('GITHUB_HEAD_REF')
|
||||
or '')
|
||||
publish = bool(re.fullmatch(r'\d+\.\d+', branch))
|
||||
with open(os.environ['GITHUB_ENV'], 'a') as env:
|
||||
env.write(f'PUBLISH_PACKAGE={"true" if publish else "false"}\n')
|
||||
if publish:
|
||||
env.write(f'PACKAGE_SERIES={branch}\n')
|
||||
if publish:
|
||||
print(f'Publishing package series: {branch}')
|
||||
else:
|
||||
print(f'Skipping package publish for branch: {branch}')
|
||||
PY
|
||||
|
||||
- name: Set up Python
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
- name: Configurar Python
|
||||
uses: https://gitea.com/actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install build tools
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build twine
|
||||
- name: Instalar herramientas
|
||||
run: python -m pip install --upgrade "build>=1,<2" "twine>=6,<7"
|
||||
|
||||
- name: Check publishing credentials
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
- name: Comprobar credenciales
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.REGISTRY_USER }}
|
||||
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
test -n "$TWINE_USERNAME" || { echo "Missing REGISTRY_USER secret"; exit 1; }
|
||||
test -n "$TWINE_PASSWORD" || { echo "Missing REGISTRY_PASSWORD secret"; exit 1; }
|
||||
run: test -n "$TWINE_PASSWORD" || { echo "Missing REGISTRY_PASSWORD secret"; exit 1; }
|
||||
|
||||
- name: Set CI package version
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
- name: Fijar versión CI
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.REGISTRY_USER }}
|
||||
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
PACKAGE_VERSION: 8.0.${{ gitea.run_number }}
|
||||
run: |
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
OWNER = 'tryton-do'
|
||||
PACKAGE_TYPE = 'pypi'
|
||||
API_ROOT = 'https://gitea.joseagrc.com/api/v1'
|
||||
|
||||
def normalize(name):
|
||||
return re.sub(r'[-_.]+', '-', name).lower()
|
||||
|
||||
cfg_path = Path('tryton.cfg')
|
||||
cfg_text = cfg_path.read_text()
|
||||
cfg_match = re.search(r'(?m)^version=(.+)$', cfg_text)
|
||||
if not cfg_match:
|
||||
raise SystemExit('tryton.cfg has no version entry')
|
||||
|
||||
series = os.environ['PACKAGE_SERIES']
|
||||
major, minor = map(int, series.split('.'))
|
||||
|
||||
pyproject = tomllib.loads(Path('pyproject.toml').read_text())
|
||||
package_name = pyproject['project']['name']
|
||||
normalized_name = normalize(package_name)
|
||||
|
||||
username = os.environ['TWINE_USERNAME']
|
||||
password = os.environ['TWINE_PASSWORD']
|
||||
token = base64.b64encode(
|
||||
f'{username}:{password}'.encode()).decode()
|
||||
|
||||
def request_json(path, params=None, missing_ok=False):
|
||||
url = f'{API_ROOT}{path}'
|
||||
if params:
|
||||
url = f'{url}?{urlencode(params)}'
|
||||
request = Request(url, headers={
|
||||
'Accept': 'application/json',
|
||||
'Authorization': f'Basic {token}',
|
||||
})
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
return json.load(response)
|
||||
except HTTPError as error:
|
||||
if missing_ok and error.code == 404:
|
||||
return []
|
||||
raise
|
||||
|
||||
def collect_versions(payload, package_specific=False):
|
||||
versions = []
|
||||
if isinstance(payload, list):
|
||||
for item in payload:
|
||||
versions.extend(collect_versions(item, package_specific))
|
||||
elif isinstance(payload, dict):
|
||||
item_name = payload.get('name') or payload.get('package_name')
|
||||
item_type = payload.get('type') or payload.get('package_type')
|
||||
item_version = payload.get('version')
|
||||
if item_version and (
|
||||
package_specific
|
||||
or (item_type == PACKAGE_TYPE
|
||||
and normalize(item_name or '') == normalized_name)):
|
||||
versions.append(str(item_version))
|
||||
for value in payload.values():
|
||||
if isinstance(value, (list, dict)):
|
||||
versions.extend(collect_versions(value, package_specific))
|
||||
return versions
|
||||
|
||||
versions = []
|
||||
for page in range(1, 11):
|
||||
payload = request_json(
|
||||
f'/packages/{OWNER}/{PACKAGE_TYPE}/{package_name}',
|
||||
{'page': page, 'limit': 100},
|
||||
missing_ok=True)
|
||||
page_versions = collect_versions(payload, package_specific=True)
|
||||
versions.extend(page_versions)
|
||||
if not page_versions:
|
||||
break
|
||||
|
||||
for page in range(1, 11):
|
||||
payload = request_json(
|
||||
f'/packages/{OWNER}',
|
||||
{'page': page, 'limit': 100, 'type': PACKAGE_TYPE,
|
||||
'q': package_name})
|
||||
page_versions = collect_versions(payload)
|
||||
versions.extend(page_versions)
|
||||
if not page_versions:
|
||||
break
|
||||
|
||||
patch_pattern = re.compile(rf'^{major}\.{minor}\.(\d+)$')
|
||||
patches = []
|
||||
for version in set(versions):
|
||||
match = patch_pattern.fullmatch(version)
|
||||
if match:
|
||||
patches.append(int(match.group(1)))
|
||||
|
||||
next_patch = max(patches) + 1 if patches else 0
|
||||
ci_version = f'{major}.{minor}.{next_patch}'
|
||||
cfg_text = re.sub(
|
||||
r'(?m)^version=.+$', f'version={ci_version}', cfg_text)
|
||||
cfg_path.write_text(cfg_text)
|
||||
print(f'Package name: {package_name}')
|
||||
print(f'Published versions found: {sorted(set(versions))}')
|
||||
print(f'Package version: {ci_version}')
|
||||
import os, re
|
||||
path = Path('tryton.cfg')
|
||||
text = path.read_text()
|
||||
path.write_text(re.sub(r'(?m)^version=.*$',
|
||||
f"version={os.environ['PACKAGE_VERSION']}", text))
|
||||
PY
|
||||
|
||||
- name: Build package
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
- name: Construir y validar
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Check package
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
run: |
|
||||
python -m twine check dist/*
|
||||
|
||||
- name: Publish to Gitea PyPI registry
|
||||
if: env.PUBLISH_PACKAGE == 'true'
|
||||
- name: Publicar en Gitea PyPI
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.REGISTRY_USER }}
|
||||
TWINE_USERNAME: unpaismejor
|
||||
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
python -m twine upload \
|
||||
--repository-url https://gitea.joseagrc.com/api/packages/tryton-do/pypi \
|
||||
python -m twine upload --non-interactive \
|
||||
--repository-url https://code.unpaismejor.org.do/api/packages/tryton-do/pypi \
|
||||
dist/*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (C) 2022 SOLUTEMA SRL
|
||||
Copyright (C) 2022-2026 Fundación Un País Mejor
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
+6
-3
@@ -12,7 +12,7 @@ Installing
|
||||
|
||||
Install the package and activate the module in the database::
|
||||
|
||||
pip install trytond_ncf_manager
|
||||
pip install unpaismejor_ncf_manager
|
||||
trytond-admin -d <database> -u ncf_manager
|
||||
|
||||
Publishing
|
||||
@@ -21,7 +21,10 @@ Publishing
|
||||
The included Gitea Actions workflow builds and publishes the package to the
|
||||
Gitea PyPI registry from version-series branches such as ``8.0``.
|
||||
|
||||
Configure these repository secrets:
|
||||
Configure this repository secret:
|
||||
|
||||
* ``REGISTRY_USER``
|
||||
* ``REGISTRY_PASSWORD``
|
||||
|
||||
The module assigns fiscal numbers transactionally from ``ir.sequence.strict``.
|
||||
It does not sign or transmit e-CF documents; transmission remains the explicit
|
||||
responsibility of an electronic invoicing connector.
|
||||
|
||||
+4
-4
@@ -2046,7 +2046,7 @@
|
||||
</table:table-row>
|
||||
<table:table-row table:style-name="Table4.2">
|
||||
<table:table-cell table:style-name="Table4.A2" table:number-columns-spanned="2" office:value-type="string">
|
||||
<text:p text:style-name="P45"><text:placeholder text:placeholder-type="text"><if test="invoice.tip_invoiced > 0"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P45"><text:placeholder text:placeholder-type="text"><if test="invoice.tip > 0"></text:placeholder></text:p>
|
||||
</table:table-cell>
|
||||
<table:covered-table-cell/>
|
||||
</table:table-row>
|
||||
@@ -2055,7 +2055,7 @@
|
||||
<text:p text:style-name="P73">Propina:</text:p>
|
||||
</table:table-cell>
|
||||
<table:table-cell table:style-name="Table4.B12" office:value-type="string">
|
||||
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text"><format_currency(invoice.tip_invoiced, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text"><format_currency(invoice.tip, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
</table:table-cell>
|
||||
</table:table-row>
|
||||
<table:table-row table:style-name="Table4.1">
|
||||
@@ -2835,7 +2835,7 @@
|
||||
</table:table-row>
|
||||
<table:table-row table:style-name="Table13.2">
|
||||
<table:table-cell table:style-name="Table13.A2" table:number-columns-spanned="2" office:value-type="string">
|
||||
<text:p text:style-name="P45"><text:placeholder text:placeholder-type="text"><if test="invoice.tip_invoiced > 0"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P45"><text:placeholder text:placeholder-type="text"><if test="invoice.tip > 0"></text:placeholder></text:p>
|
||||
</table:table-cell>
|
||||
<table:covered-table-cell/>
|
||||
</table:table-row>
|
||||
@@ -2844,7 +2844,7 @@
|
||||
<text:p text:style-name="P73">Propina:</text:p>
|
||||
</table:table-cell>
|
||||
<table:table-cell table:style-name="Table13.B12" office:value-type="string">
|
||||
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text"><format_currency(invoice.tip_invoiced, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
<text:p text:style-name="P71"><text:placeholder text:placeholder-type="text"><format_currency(invoice.tip, invoice.party.lang, invoice.currency)></text:placeholder></text:p>
|
||||
</table:table-cell>
|
||||
</table:table-row>
|
||||
<table:table-row table:style-name="Table13.1">
|
||||
|
||||
+52
-1
@@ -3,6 +3,7 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
import re
|
||||
from sql.aggregate import Sum
|
||||
from sql.conditionals import Coalesce
|
||||
from sql.functions import Round
|
||||
@@ -1563,7 +1564,7 @@ class Invoice(metaclass=PoolMeta):
|
||||
|
||||
ncf_code = invoice.ncf_type
|
||||
|
||||
if invoice.type == 'in' and ncf_code == '99':
|
||||
if ncf_code == '99':
|
||||
continue
|
||||
|
||||
ncf_sequence = invoice.ncf_sequence or cls._ncf_find_active_sequence(
|
||||
@@ -1712,6 +1713,53 @@ class Invoice(metaclass=PoolMeta):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_incorrect_lenght_ncf_number_modified'))
|
||||
|
||||
@staticmethod
|
||||
def _ncf_normalize_number(number):
|
||||
return (number or '').strip().upper()
|
||||
|
||||
@classmethod
|
||||
def _ncf_validate_number(cls, invoice):
|
||||
number = cls._ncf_normalize_number(invoice.ncf_number)
|
||||
if not number:
|
||||
return
|
||||
legacy_supplier_ncf = (
|
||||
invoice.type == 'in' and re.fullmatch(r'[A-Z]\d{18}', number))
|
||||
if not legacy_supplier_ncf and not re.fullmatch(
|
||||
r'(?:B\d{10}|E\d{12})', number):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_invalid_ncf_structure', number=number))
|
||||
if legacy_supplier_ncf:
|
||||
invoice.ncf_number = number
|
||||
return
|
||||
expected = ('E' if invoice.ncf_type in _ECF_CODES else 'B') + \
|
||||
invoice.ncf_type.zfill(2)
|
||||
if invoice.ncf_type != '99' and not number.startswith(expected):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_invalid_ncf_type_prefix',
|
||||
number=number, prefix=expected))
|
||||
invoice.ncf_number = number
|
||||
|
||||
@classmethod
|
||||
def _ncf_check_duplicate(cls, invoice):
|
||||
if not invoice.ncf_number:
|
||||
return
|
||||
domain = [
|
||||
('id', '!=', invoice.id),
|
||||
('company', '=', invoice.company.id),
|
||||
('ncf_number', '=', invoice.ncf_number),
|
||||
]
|
||||
# Supplier NCF sequences are unique per issuer, not globally.
|
||||
if invoice.type == 'in':
|
||||
domain.extend([
|
||||
('type', '=', 'in'),
|
||||
('party', '=', invoice.party.id),
|
||||
])
|
||||
else:
|
||||
domain.append(('type', '=', 'out'))
|
||||
if cls.search(domain, limit=1):
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_duplicated_ncf_number_invoice'))
|
||||
|
||||
@staticmethod
|
||||
def _ncf_is_fiscal_cancellation_required(invoice):
|
||||
return (
|
||||
@@ -1782,6 +1830,8 @@ class Invoice(metaclass=PoolMeta):
|
||||
cls.set_ncf_number(invoices)
|
||||
|
||||
for invoice in invoices:
|
||||
cls._ncf_validate_number(invoice)
|
||||
cls._ncf_check_duplicate(invoice)
|
||||
ncf_code = invoice.ncf_type
|
||||
is_debit_note = cls._ncf_is_debit_note(invoice)
|
||||
is_credit_note = cls._ncf_is_credit_note(invoice)
|
||||
@@ -1789,6 +1839,7 @@ class Invoice(metaclass=PoolMeta):
|
||||
is_without_ncf = ncf_code == '99'
|
||||
|
||||
if invoice.type == 'out':
|
||||
if not is_without_ncf:
|
||||
invoice._ncf_validate_selected_sequence()
|
||||
if is_credit_note:
|
||||
invoice.check_credit_note()
|
||||
|
||||
+21
@@ -105,5 +105,26 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<record model="ir.message" id="msg_partial_credit_note_refund_not_allowed">
|
||||
<field name="text">Partial credit note refund is not allowed.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_ncf_structure">
|
||||
<field name="text">The fiscal number "%(number)s" must have the DGII B + 10 digits or E + 12 digits structure.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_invalid_ncf_type_prefix">
|
||||
<field name="text">The fiscal number "%(number)s" does not match the expected prefix "%(prefix)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_company_mismatch">
|
||||
<field name="text">The fiscal sequence and its authorization must belong to the same company.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_sequence_prefix_mismatch">
|
||||
<field name="text">The fiscal sequence must use prefix "%(prefix)s" for the selected NCF type.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_invalid_padding">
|
||||
<field name="text">The fiscal sequence must use a padding of %(padding)s digits.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_invalid_range">
|
||||
<field name="text">The authorized fiscal range must start at 1 or greater and end at or after its start.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_ncf_next_before_range">
|
||||
<field name="text">The next fiscal number cannot be before the authorized range.</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
+25
-1
@@ -1,8 +1,10 @@
|
||||
# This file is part of ncf_manager module for Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import ModelView, ModelSQL, fields
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, Id
|
||||
from trytond.pyson import Eval
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
@@ -146,3 +148,25 @@ class NCFManagerSequences(ModelSQL, ModelView):
|
||||
if not self.ncf_type_string:
|
||||
return ''
|
||||
return self.ncf_type_string.split('-')[0]
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, records, field_names):
|
||||
super().validate_fields(records, field_names)
|
||||
if field_names & {'pos', 'ncf_sequences', 'ncf_type'}:
|
||||
for record in records:
|
||||
record._validate_fiscal_sequence()
|
||||
|
||||
def _validate_fiscal_sequence(self):
|
||||
sequence = self.ncf_sequences
|
||||
if not sequence:
|
||||
return
|
||||
if sequence.company != self.pos.company:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_company_mismatch'))
|
||||
expected_prefix = ('E' if self.ncf_type in {
|
||||
'31', '32', '33', '34', '41', '43', '44', '45', '46', '47'
|
||||
} else 'B') + self.ncf_type.zfill(2)
|
||||
if self.ncf_type != '99' and sequence.prefix != expected_prefix:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_sequence_prefix_mismatch',
|
||||
prefix=expected_prefix))
|
||||
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
[build-system]
|
||||
requires = ['hatchling >= 1', 'hatch-tryton']
|
||||
requires = ['hatchling >=1,<1.31', 'hatch-tryton']
|
||||
build-backend = 'hatchling.build'
|
||||
|
||||
[project]
|
||||
name = 'trytond_ncf_manager'
|
||||
name = 'unpaismejor_ncf_manager'
|
||||
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
|
||||
requires-python = '>=3.10'
|
||||
requires-python = '>=3.11'
|
||||
maintainers = [
|
||||
{name = "Solutema"},
|
||||
{name = "Fundación Un País Mejor"},
|
||||
]
|
||||
description = "NCF management for Tryton"
|
||||
license = 'GPL-3.0-or-later'
|
||||
@@ -26,8 +26,8 @@ classifiers = [
|
||||
ncf_manager = 'trytond.modules.ncf_manager'
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://www.tryton.org/"
|
||||
repository = "https://gitea.joseagrc.com/tryton-do/ncf_manager"
|
||||
homepage = "https://code.unpaismejor.org.do/tryton-do/ncf_manager"
|
||||
repository = "https://code.unpaismejor.org.do/tryton-do/ncf_manager.git"
|
||||
|
||||
[tool.hatch.build]
|
||||
include = [
|
||||
|
||||
+30
-12
@@ -1,18 +1,10 @@
|
||||
# This file is part of ncf_manager module for Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from string import Template
|
||||
import time
|
||||
|
||||
from sql import Literal, For
|
||||
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.model.exceptions import ValidationError
|
||||
from trytond.model import ModelView, ModelSQL, DeactivableMixin, fields, Check
|
||||
from trytond.pyson import Eval, And
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.model import fields
|
||||
from trytond.pyson import Eval
|
||||
from trytond.pool import PoolMeta
|
||||
from trytond.i18n import gettext
|
||||
from trytond import backend
|
||||
|
||||
# Maps sequence_type.name → (prefix, padding)
|
||||
_NCF_TYPE_MAP = {
|
||||
@@ -38,6 +30,7 @@ _NCF_TYPE_MAP = {
|
||||
'Exportación Electrónico': ('E46', 10),
|
||||
'Pagos al Exterior Electrónico': ('E47', 10),
|
||||
}
|
||||
NCF_PREFIXES = tuple(prefix for prefix, _padding in _NCF_TYPE_MAP.values())
|
||||
|
||||
|
||||
class Sequence(metaclass=PoolMeta):
|
||||
@@ -114,9 +107,34 @@ class Sequence(metaclass=PoolMeta):
|
||||
|
||||
@fields.depends('number_next', 'until_number')
|
||||
def on_change_with_remaining_qty(self, name=None):
|
||||
if self.until_number and self.number_next:
|
||||
if self.until_number is not None and self.number_next is not None:
|
||||
return self.until_number - self.number_next + 1
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, records, field_names):
|
||||
super().validate_fields(records, field_names)
|
||||
if field_names & {
|
||||
'prefix', 'padding', 'from_number', 'until_number',
|
||||
'number_next'}:
|
||||
for record in records:
|
||||
record._validate_ncf_range()
|
||||
|
||||
def _validate_ncf_range(self):
|
||||
if self.prefix not in NCF_PREFIXES:
|
||||
return
|
||||
expected_padding = 10 if self.prefix.startswith('E') else 8
|
||||
if self.padding != expected_padding:
|
||||
raise UserError(gettext(
|
||||
'ncf_manager.msg_ncf_invalid_padding',
|
||||
padding=expected_padding))
|
||||
if self.from_number is None or self.until_number is None:
|
||||
return
|
||||
if self.from_number < 1 or self.until_number < self.from_number:
|
||||
raise UserError(gettext('ncf_manager.msg_ncf_invalid_range'))
|
||||
if (self.number_next is not None
|
||||
and self.number_next < self.from_number):
|
||||
raise UserError(gettext('ncf_manager.msg_ncf_next_before_range'))
|
||||
|
||||
class SequenceStrict(Sequence):
|
||||
"Sequence Strict"
|
||||
__name__ = 'ir.sequence.strict'
|
||||
|
||||
+28
-3
@@ -50,7 +50,7 @@ class NcfManagerTestCase(ModuleTestCase):
|
||||
session_id, _start, _end = CreateChart.create()
|
||||
chart = CreateChart(session_id)
|
||||
chart.account.account_template = AccountTemplate(ModelData.get_id(
|
||||
'account_do', 'do_account_root'))
|
||||
'account_do', 'do_account_root_en'))
|
||||
chart.account.company = company
|
||||
chart.transition_create_account()
|
||||
|
||||
@@ -228,8 +228,8 @@ class NcfManagerUnitTestCase(unittest.TestCase):
|
||||
|
||||
self.assertNotIn('line.itbis_line', template)
|
||||
self.assertEqual(template.count('line.itbis_invoiced_line'), 2)
|
||||
self.assertEqual(template.count('if test="invoice.tip_invoiced'), 2)
|
||||
self.assertEqual(template.count('format_currency(invoice.tip_invoiced'), 2)
|
||||
self.assertEqual(template.count('if test="invoice.tip'), 2)
|
||||
self.assertEqual(template.count('format_currency(invoice.tip'), 2)
|
||||
|
||||
def test_sequence_defaults_match_ncf_type(self):
|
||||
sequence = SimpleNamespace(
|
||||
@@ -247,6 +247,31 @@ class NcfManagerUnitTestCase(unittest.TestCase):
|
||||
self.assertEqual(Sequence.on_change_with_approved_qty(sequence), 10)
|
||||
self.assertEqual(Sequence.on_change_with_remaining_qty(sequence), 7)
|
||||
|
||||
def test_ncf_structure_and_normalization(self):
|
||||
invoice = SimpleNamespace(
|
||||
ncf_number=' b0100000001 ', ncf_type='1')
|
||||
Invoice._ncf_validate_number(invoice)
|
||||
self.assertEqual(invoice.ncf_number, 'B0100000001')
|
||||
|
||||
invoice.ncf_number = 'E310000000001'
|
||||
invoice.ncf_type = '31'
|
||||
Invoice._ncf_validate_number(invoice)
|
||||
|
||||
def test_ncf_rejects_invalid_structure_or_type(self):
|
||||
invoice = SimpleNamespace(ncf_number='B010001', ncf_type='1')
|
||||
with self.assertRaises(UserError):
|
||||
Invoice._ncf_validate_number(invoice)
|
||||
|
||||
def test_legacy_supplier_ncf_is_preserved_for_migration(self):
|
||||
invoice = SimpleNamespace(
|
||||
type='in', ncf_number='A010010010100000001', ncf_type='1')
|
||||
Invoice._ncf_validate_number(invoice)
|
||||
self.assertEqual(invoice.ncf_number, 'A010010010100000001')
|
||||
|
||||
invoice.ncf_number = 'B0200000001'
|
||||
with self.assertRaises(UserError):
|
||||
Invoice._ncf_validate_number(invoice)
|
||||
|
||||
def test_fiscal_cancellation_requires_ncf_number(self):
|
||||
fiscal_invoice = SimpleNamespace(type='out', ncf_number='B0100000001')
|
||||
non_fiscal_invoice = SimpleNamespace(type='out', ncf_number=None)
|
||||
|
||||
Reference in New Issue
Block a user