-
Notifications
You must be signed in to change notification settings - Fork 0
Enhancement/11 move vat rates #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
3d1e48d
f45ef1d
8b3adc7
9696462
1cb9f87
0dcd614
12ad19a
ca9ce85
d631d34
7df0c08
c9d6052
106b503
900a182
662a6a7
95f3550
acb0fdb
aa85a94
81e7ba7
2cd45ca
3718f20
370809d
35f537e
2ce4ef7
26c8bd5
1cdf199
9d2431b
4601aa6
6f53399
084ced6
4035704
6aadd1a
edcc349
bae70a3
528f133
5373fd5
682258f
7013fda
5f7f3bd
3fb7220
bf3947a
eef3a89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| name: Run Tests | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - master | ||
| pull_request_target: | ||
| branches: | ||
| - master | ||
| push: | ||
| branches: | ||
| - master | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| python-version: ['3.10'] | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | ||
|
|
||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -e . | ||
| pip install pytest | ||
|
|
||
| - name: Run tests | ||
| run: | | ||
| python -m pytest tests/ -v | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -67,3 +67,69 @@ Usage | |||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| For more detailed documentation, see the `full pyvat documentation <http://pyvat.readthedocs.org/>`_. | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Running Tests | ||||||||||||||||
| ------------- | ||||||||||||||||
|
|
||||||||||||||||
| ``pyvat`` uses `pytest <https://pytest.org/>`_ for testing. To run the test suite: | ||||||||||||||||
|
|
||||||||||||||||
| **Install test dependencies:** | ||||||||||||||||
|
|
||||||||||||||||
| .. code-block:: bash | ||||||||||||||||
|
|
||||||||||||||||
| $ pip install pytest | ||||||||||||||||
|
|
||||||||||||||||
| **Run all tests:** | ||||||||||||||||
|
|
||||||||||||||||
| .. code-block:: bash | ||||||||||||||||
|
|
||||||||||||||||
| $ python -m pytest tests/ -v | ||||||||||||||||
|
|
||||||||||||||||
| **Run specific test files:** | ||||||||||||||||
|
|
||||||||||||||||
| .. code-block:: bash | ||||||||||||||||
|
|
||||||||||||||||
| # Test new countries implementation | ||||||||||||||||
| $ python -m pytest tests/test_new_countries.py -v | ||||||||||||||||
|
|
||||||||||||||||
| # Test VAT charge calculations | ||||||||||||||||
| $ python -m pytest tests/test_sale_vat_charge.py -v | ||||||||||||||||
|
|
||||||||||||||||
| # Test VAT number validators | ||||||||||||||||
| $ python -m pytest tests/test_validators.py -v | ||||||||||||||||
|
|
||||||||||||||||
| **Run with coverage:** | ||||||||||||||||
|
|
||||||||||||||||
| .. code-block:: bash | ||||||||||||||||
|
|
||||||||||||||||
| $ pip install pytest-cov | ||||||||||||||||
| $ python -m pytest tests/ --cov=pyvat --cov-report=html | ||||||||||||||||
|
|
||||||||||||||||
| **Test Results:** | ||||||||||||||||
|
|
||||||||||||||||
| The test suite includes: | ||||||||||||||||
|
|
||||||||||||||||
| * **VAT Rules Tests**: Verify correct VAT rates for all supported countries | ||||||||||||||||
| * **Registry Tests**: Test B2B exemption logic for different countries | ||||||||||||||||
| * **VAT Charge Tests**: Ensure proper VAT calculation for cross-border sales | ||||||||||||||||
| * **Validator Tests**: Check VAT number format validation | ||||||||||||||||
|
|
||||||||||||||||
| *Note: Some validator tests that call external VIES API are currently skipped and will be refactored with mocks.* | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Supported Countries | ||||||||||||||||
| ------------------- | ||||||||||||||||
|
|
||||||||||||||||
| **EU Countries**: All EU member states with full VIES integration | ||||||||||||||||
|
|
||||||||||||||||
| **Non-EU Countries**: | ||||||||||||||||
|
|
||||||||||||||||
| * Egypt (EG) - 14% VAT, B2B exempt | ||||||||||||||||
| * Switzerland (CH) - 8.1% VAT, B2B not exempt | ||||||||||||||||
| * Canada (CA) - 0% VAT, B2B exempt | ||||||||||||||||
| * Norway (NO) - 25% VAT, B2B exempt | ||||||||||||||||
| * Monaco (MC) - 20% VAT, B2B not exempt | ||||||||||||||||
| * French Overseas Departments (RE, GP, MQ) - 8.5% VAT, B2B not exempt | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| * Monaco (MC) - 20% VAT, B2B not exempt | |
| * French Overseas Departments (RE, GP, MQ) - 8.5% VAT, B2B not exempt | |
| * French Overseas Departments (RE, GP, MQ) - 8.5% VAT, B2B not exempt | |
| **French VAT Zone Countries**: | |
| * Monaco (MC) - 20% VAT, B2B not exempt (Treated as part of the French VAT zone; VAT numbers are validated using the French VIES registry. Monaco is not considered a non-EU country for VAT purposes.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |||||
|
|
||||||
| from .item_type import ItemType | ||||||
| from .party import Party | ||||||
| from .registries import ViesRegistry, HMRCRegistry, EgyptRegistry | ||||||
| from .registries import ViesRegistry, HMRCRegistry, EgyptRegistry, SwitzerlandRegistry, CanadaRegistry, NorwayRegistry, MonacoRegistry, DomRegistry | ||||||
|
|
||||||
| from .result import VatNumberCheckResult | ||||||
| from .vat_charge import VatCharge, VatChargeAction | ||||||
|
|
@@ -50,7 +50,6 @@ | |||||
| "SE": re.compile(r"^\d{12}$"), | ||||||
| "SI": re.compile(r"^\d{8}$"), | ||||||
| "SK": re.compile(r"^\d{10}$"), | ||||||
| 'EG': re.compile(r'^\d{9}$'), | ||||||
|
|
||||||
| } | ||||||
| """VAT number expressions. | ||||||
|
|
@@ -75,6 +74,26 @@ | |||||
| """Egypt Registry instance. | ||||||
|
||||||
| """Egypt Registry instance. | |
| """Egypt Registry instance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Format
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,6 @@ | |
| 'SE', # Sweden. | ||
| 'SI', # Slovenia. | ||
| 'SK', # Slovakia. | ||
| 'EG', # Egypt. | ||
| ]) | ||
| """EU country codes. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -366,22 +366,98 @@ def get_vat_rate(self, item_type, postal_code=None): | |
| return Decimal(7) | ||
| return Decimal(19) | ||
|
|
||
| class EgVatRules(): | ||
| """VAT rules for Egypt. | ||
|
|
||
| class NonEuVatRules(object): | ||
| """Base class for non-EU countries VAT rules. | ||
|
|
||
| Provides default implementation for countries that charge VAT | ||
| both when selling TO and FROM the country. | ||
| """ | ||
|
|
||
| def __init__(self, vat_rate): | ||
| """Initialize with a constant VAT rate. | ||
|
|
||
| :param vat_rate: The VAT rate percentage for the country. | ||
| :type vat_rate: int, float, or Decimal | ||
| """ | ||
| self.vat_rate = ensure_decimal(vat_rate) | ||
|
|
||
| def get_sale_to_country_vat_charge(self, | ||
| date, | ||
| item_type, | ||
| buyer, | ||
| seller, | ||
| postal_code=None): | ||
| """Get VAT charge when selling TO this country.""" | ||
| return VatCharge(VatChargeAction.charge, | ||
| buyer.country_code, | ||
| self.get_vat_rate(item_type)) | ||
| self.get_vat_rate(item_type, postal_code)) | ||
|
|
||
| def get_sale_from_country_vat_charge(self, | ||
| date, | ||
| item_type, | ||
| buyer, | ||
| seller, | ||
| postal_code=None): | ||
| """Get VAT charge when selling FROM this country.""" | ||
| return VatCharge(VatChargeAction.charge, | ||
| buyer.country_code, | ||
| self.get_vat_rate(item_type, postal_code)) | ||
|
|
||
| def get_vat_rate(self, item_type, postal_code=None): | ||
| """Get the VAT rate for an item type. | ||
|
|
||
| :param item_type: Item type. | ||
| :type item_type: ItemType | ||
| :param postal_code: Postal code (for region-specific rates). | ||
| :type postal_code: str | ||
| :returns: VAT rate in percent. | ||
| :rtype: Decimal | ||
| """ | ||
| return self.vat_rate | ||
|
|
||
|
|
||
| class EgVatRules(NonEuVatRules): | ||
| """VAT rules for Egypt.""" | ||
|
|
||
| def __init__(self): | ||
| super(EgVatRules, self).__init__(14) | ||
|
|
||
|
|
||
| class ChVatRules(NonEuVatRules): | ||
| """VAT rules for Switzerland.""" | ||
|
|
||
| def __init__(self): | ||
| super(ChVatRules, self).__init__(Decimal('8.1')) | ||
|
|
||
|
|
||
| class CaVatRules(NonEuVatRules): | ||
| """VAT rules for Canada.""" | ||
|
|
||
| def __init__(self): | ||
| super(CaVatRules, self).__init__(0) | ||
|
|
||
|
|
||
| class NoVatRules(NonEuVatRules): | ||
| """VAT rules for Norway.""" | ||
|
|
||
| def __init__(self): | ||
| super(NoVatRules, self).__init__(25) | ||
|
|
||
|
|
||
| class MoVatRules(NonEuVatRules): | ||
| """VAT rules for Monaco.""" | ||
|
|
||
| def __init__(self): | ||
| super(MoVatRules, self).__init__(20) | ||
|
|
||
|
|
||
| class DomVatRules(NonEuVatRules): | ||
| """VAT rules for DOM (French Overseas Departments: Réunion, Guadeloupe, Martinique).""" | ||
|
|
||
| def __init__(self): | ||
| super(DomVatRules, self).__init__(Decimal('8.5')) | ||
|
|
||
| def get_vat_rate(self, item_type): | ||
| return Decimal(14) | ||
|
|
||
| # VAT rates updated July 1st 2025 | ||
| VAT_RULES = { | ||
|
|
@@ -415,6 +491,13 @@ def get_vat_rate(self, item_type): | |
| 'SK': ConstantEuVatRateRules(23), | ||
| 'SI': ConstantEuVatRateRules(22), | ||
| 'EG': EgVatRules(), | ||
| 'CH': ChVatRules(), | ||
| 'CA': CaVatRules(), | ||
| 'NO': NoVatRules(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not confortable with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mostafa-hisham Please rename the shortnames to country:
So that we can avoid the misleading |
||
| 'MC': MoVatRules(), | ||
| 'RE': DomVatRules(), # Réunion | ||
| 'GP': DomVatRules(), # Guadeloupe | ||
| 'MQ': DomVatRules(), # Martinique | ||
| } | ||
|
|
||
| """VAT rules by country. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using both 'pull_request' and 'pull_request_target' triggers can lead to security concerns. The 'pull_request_target' event runs in the context of the base repository and has access to secrets, which can be dangerous when combined with 'pull_request'. Unless there's a specific need for 'pull_request_target', it should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would apply suggestion