mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 22:35:36 +00:00
Airtable import currency field improvements
This commit is contained in:
parent
8b71d7c709
commit
c88c316d63
5 changed files with 324 additions and 11 deletions
backend
src/baserow/contrib/database/airtable
tests/baserow/contrib/database/airtable
changelog/entries/unreleased/bug
|
@ -1,5 +1,5 @@
|
|||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -28,6 +28,7 @@ from baserow.contrib.database.fields.models import (
|
|||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
|
||||
from .config import AirtableImportConfig
|
||||
from .constants import AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING
|
||||
from .helpers import import_airtable_date_type_options, set_select_options_on_field
|
||||
from .import_report import (
|
||||
ERROR_TYPE_DATA_TYPE_MISMATCH,
|
||||
|
@ -155,17 +156,37 @@ class NumberAirtableColumnType(AirtableColumnType):
|
|||
self, raw_airtable_table, raw_airtable_column, config, import_report
|
||||
):
|
||||
type_options = raw_airtable_column.get("typeOptions", {})
|
||||
decimal_places = 0
|
||||
options_format = type_options.get("format", "")
|
||||
suffix = ""
|
||||
|
||||
if type_options.get("format", "integer") == "decimal":
|
||||
# Minimum of 1 and maximum of 5 decimal places.
|
||||
decimal_places = min(
|
||||
max(1, type_options.get("precision", 1)), NUMBER_MAX_DECIMAL_PLACES
|
||||
if "percent" in options_format:
|
||||
suffix = "%"
|
||||
|
||||
decimal_places = min(
|
||||
max(0, type_options.get("precision", 0)), NUMBER_MAX_DECIMAL_PLACES
|
||||
)
|
||||
prefix = type_options.get("symbol", "")
|
||||
separator_format = type_options.get("separatorFormat", "")
|
||||
number_separator = AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING.get(
|
||||
separator_format, ""
|
||||
)
|
||||
|
||||
if separator_format != "" and number_separator == "":
|
||||
import_report.add_failed(
|
||||
f"Number field: \"{raw_airtable_column['name']}\"",
|
||||
SCOPE_FIELD,
|
||||
raw_airtable_table.get("name", ""),
|
||||
ERROR_TYPE_UNSUPPORTED_FEATURE,
|
||||
f"The field was imported, but the separator format "
|
||||
f"{separator_format} was dropped because it doesn't exist in Baserow.",
|
||||
)
|
||||
|
||||
return NumberField(
|
||||
number_decimal_places=decimal_places,
|
||||
number_negative=type_options.get("negative", True),
|
||||
number_prefix=prefix,
|
||||
number_suffix=suffix,
|
||||
number_separator=number_separator,
|
||||
)
|
||||
|
||||
def to_baserow_export_serialized_value(
|
||||
|
@ -180,13 +201,38 @@ class NumberAirtableColumnType(AirtableColumnType):
|
|||
config,
|
||||
import_report,
|
||||
):
|
||||
if value is not None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
value = Decimal(value)
|
||||
except InvalidOperation:
|
||||
# If the value can't be parsed as decimal, then it might be corrupt, so we
|
||||
# need to inform the user and skip the import.
|
||||
row_name = get_airtable_row_primary_value(
|
||||
raw_airtable_table, raw_airtable_row
|
||||
)
|
||||
import_report.add_failed(
|
||||
f"Row: \"{row_name}\", field: \"{raw_airtable_column['name']}\"",
|
||||
SCOPE_CELL,
|
||||
raw_airtable_table["name"],
|
||||
ERROR_TYPE_DATA_TYPE_MISMATCH,
|
||||
f"Cell value was left empty because the numeric value {value} "
|
||||
f'could not be parsed"',
|
||||
)
|
||||
return None
|
||||
|
||||
if value is not None and not baserow_field.number_negative and value < 0:
|
||||
value = None
|
||||
# Airtable stores 10% as 0.1, so we would need to multiply it by 100 so get the
|
||||
# correct value in Baserow.
|
||||
type_options = raw_airtable_column.get("typeOptions", {})
|
||||
options_format = type_options.get("format", "")
|
||||
if "percent" in options_format:
|
||||
value = value * 100
|
||||
|
||||
return None if value is None else str(value)
|
||||
if not baserow_field.number_negative and value < 0:
|
||||
return None
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
class RatingAirtableColumnType(AirtableColumnType):
|
||||
|
|
|
@ -13,3 +13,9 @@ AIRTABLE_BASEROW_COLOR_MAPPING = {
|
|||
"purple": "dark-blue",
|
||||
"gray": "light-gray",
|
||||
}
|
||||
AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING = {
|
||||
"commaPeriod": "COMMA_PERIOD",
|
||||
"periodComma": "PERIOD_COMMA",
|
||||
"spaceComma": "SPACE_COMMA",
|
||||
"spacePeriod": "SPACE_PERIOD",
|
||||
}
|
||||
|
|
|
@ -1196,6 +1196,9 @@ def test_airtable_import_number_integer_column(data_fixture, api_client):
|
|||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 0
|
||||
assert baserow_field.number_negative is False
|
||||
assert baserow_field.number_separator == ""
|
||||
assert baserow_field.number_prefix == ""
|
||||
assert baserow_field.number_suffix == ""
|
||||
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
|
@ -1269,6 +1272,50 @@ def test_airtable_import_number_integer_column(data_fixture, api_client):
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@responses.activate
|
||||
def test_airtable_import_number_invalid_number(data_fixture, api_client):
|
||||
airtable_field = {
|
||||
"id": "fldZBmr4L45mhjILhlA",
|
||||
"name": "Number",
|
||||
"type": "number",
|
||||
"typeOptions": {
|
||||
"format": "integer",
|
||||
"negative": False,
|
||||
"validatorName": "positive",
|
||||
},
|
||||
}
|
||||
(
|
||||
baserow_field,
|
||||
airtable_column_type,
|
||||
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||
{},
|
||||
airtable_field,
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
|
||||
import_report = AirtableImportReport()
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{},
|
||||
{"name": "Test"},
|
||||
{"id": "row1"},
|
||||
airtable_field,
|
||||
baserow_field,
|
||||
"INVALID_NUMBER",
|
||||
{},
|
||||
AirtableImportConfig(),
|
||||
import_report,
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert len(import_report.items) == 1
|
||||
assert import_report.items[0].object_name == 'Row: "row1", field: "Number"'
|
||||
assert import_report.items[0].scope == SCOPE_CELL
|
||||
assert import_report.items[0].table == "Test"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@responses.activate
|
||||
def test_airtable_import_number_decimal_column(data_fixture, api_client):
|
||||
|
@ -1293,7 +1340,7 @@ def test_airtable_import_number_decimal_column(data_fixture, api_client):
|
|||
)
|
||||
assert isinstance(baserow_field, NumberField)
|
||||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 1
|
||||
assert baserow_field.number_decimal_places == 0
|
||||
assert baserow_field.number_negative is False
|
||||
|
||||
airtable_field = {
|
||||
|
@ -1319,6 +1366,9 @@ def test_airtable_import_number_decimal_column(data_fixture, api_client):
|
|||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 2
|
||||
assert baserow_field.number_negative is True
|
||||
assert baserow_field.number_separator == ""
|
||||
assert baserow_field.number_prefix == ""
|
||||
assert baserow_field.number_suffix == ""
|
||||
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
|
@ -1414,6 +1464,203 @@ def test_airtable_import_number_decimal_column(data_fixture, api_client):
|
|||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 10
|
||||
assert baserow_field.number_negative is True
|
||||
assert baserow_field.number_separator == ""
|
||||
assert baserow_field.number_prefix == ""
|
||||
assert baserow_field.number_suffix == ""
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@responses.activate
|
||||
def test_airtable_import_currency_column(data_fixture, api_client):
|
||||
airtable_field = {
|
||||
"id": "fldZBmr4L45mhjILhlA",
|
||||
"name": "Currency",
|
||||
"type": "number",
|
||||
"typeOptions": {
|
||||
"format": "currency",
|
||||
"precision": 3,
|
||||
"symbol": "$",
|
||||
"separatorFormat": "commaPeriod",
|
||||
"negative": False,
|
||||
},
|
||||
}
|
||||
(
|
||||
baserow_field,
|
||||
airtable_column_type,
|
||||
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||
{},
|
||||
airtable_field,
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
assert isinstance(baserow_field, NumberField)
|
||||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 3
|
||||
assert baserow_field.number_negative is False
|
||||
assert baserow_field.number_separator == "COMMA_PERIOD"
|
||||
assert baserow_field.number_prefix == "$"
|
||||
assert baserow_field.number_suffix == ""
|
||||
|
||||
airtable_field = {
|
||||
"id": "fldZBmr4L45mhjILhlA",
|
||||
"name": "Currency",
|
||||
"type": "number",
|
||||
"typeOptions": {
|
||||
"format": "currency",
|
||||
"precision": 2,
|
||||
"symbol": "€",
|
||||
"separatorFormat": "spacePeriod",
|
||||
"negative": True,
|
||||
},
|
||||
}
|
||||
(
|
||||
baserow_field,
|
||||
airtable_column_type,
|
||||
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||
{},
|
||||
airtable_field,
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
assert isinstance(baserow_field, NumberField)
|
||||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 2
|
||||
assert baserow_field.number_negative is True
|
||||
assert baserow_field.number_separator == "SPACE_PERIOD"
|
||||
assert baserow_field.number_prefix == "€"
|
||||
assert baserow_field.number_suffix == ""
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@responses.activate
|
||||
def test_airtable_import_currency_column_non_existing_separator_format(
|
||||
data_fixture, api_client
|
||||
):
|
||||
airtable_field = {
|
||||
"id": "fldZBmr4L45mhjILhlA",
|
||||
"name": "Currency",
|
||||
"type": "number",
|
||||
"typeOptions": {
|
||||
"format": "currency",
|
||||
"precision": 3,
|
||||
"symbol": "$",
|
||||
"separatorFormat": "TEST",
|
||||
"negative": False,
|
||||
},
|
||||
}
|
||||
import_report = AirtableImportReport()
|
||||
airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||
{},
|
||||
airtable_field,
|
||||
AirtableImportConfig(),
|
||||
import_report,
|
||||
)
|
||||
assert len(import_report.items) == 1
|
||||
assert import_report.items[0].object_name == 'Number field: "Currency"'
|
||||
assert import_report.items[0].scope == SCOPE_FIELD
|
||||
assert import_report.items[0].table == ""
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@responses.activate
|
||||
def test_airtable_import_percentage_column(data_fixture, api_client):
|
||||
airtable_field = {
|
||||
"id": "fldZBmr4L45mhjILhlA",
|
||||
"name": "Currency",
|
||||
"type": "number",
|
||||
"typeOptions": {
|
||||
"format": "percentage",
|
||||
"precision": 1,
|
||||
"negative": False,
|
||||
},
|
||||
}
|
||||
(
|
||||
baserow_field,
|
||||
airtable_column_type,
|
||||
) = airtable_column_type_registry.from_airtable_column_to_serialized(
|
||||
{},
|
||||
airtable_field,
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
assert isinstance(baserow_field, NumberField)
|
||||
assert isinstance(airtable_column_type, NumberAirtableColumnType)
|
||||
assert baserow_field.number_decimal_places == 1
|
||||
assert baserow_field.number_negative is False
|
||||
assert baserow_field.number_separator == ""
|
||||
assert baserow_field.number_prefix == ""
|
||||
assert baserow_field.number_suffix == "%"
|
||||
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{},
|
||||
{"name": "Test"},
|
||||
{"id": "row1"},
|
||||
airtable_field,
|
||||
baserow_field,
|
||||
0.5,
|
||||
{},
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
== "50.0"
|
||||
)
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{},
|
||||
{"name": "Test"},
|
||||
{"id": "row1"},
|
||||
airtable_field,
|
||||
baserow_field,
|
||||
0.5,
|
||||
{},
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
== "50.0"
|
||||
)
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{},
|
||||
{"name": "Test"},
|
||||
{"id": "row1"},
|
||||
airtable_field,
|
||||
baserow_field,
|
||||
"0.05",
|
||||
{},
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
== "5.00"
|
||||
)
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{},
|
||||
{"name": "Test"},
|
||||
{"id": "row1"},
|
||||
airtable_field,
|
||||
baserow_field,
|
||||
"",
|
||||
{},
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
airtable_column_type.to_baserow_export_serialized_value(
|
||||
{},
|
||||
{"name": "Test"},
|
||||
{"id": "row1"},
|
||||
airtable_field,
|
||||
baserow_field,
|
||||
None,
|
||||
{},
|
||||
AirtableImportConfig(),
|
||||
AirtableImportReport(),
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "bug",
|
||||
"message": "Preserve the precision of the currency field in the Airtable import.",
|
||||
"issue_number": 1058,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-02-09"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "bug",
|
||||
"message": "Preserve the currency symbol and formatting of the currency field in Airtable import.",
|
||||
"issue_number": 3395,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-02-09"
|
||||
}
|
Loading…
Add table
Reference in a new issue