1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Airtable import currency field improvements

This commit is contained in:
Bram Wiepjes 2025-02-14 08:44:33 +00:00
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

View file

@ -1,5 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from decimal import Decimal from decimal import Decimal, InvalidOperation
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from django.core.exceptions import ValidationError 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 baserow.contrib.database.fields.registries import field_type_registry
from .config import AirtableImportConfig 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 .helpers import import_airtable_date_type_options, set_select_options_on_field
from .import_report import ( from .import_report import (
ERROR_TYPE_DATA_TYPE_MISMATCH, ERROR_TYPE_DATA_TYPE_MISMATCH,
@ -155,17 +156,37 @@ class NumberAirtableColumnType(AirtableColumnType):
self, raw_airtable_table, raw_airtable_column, config, import_report self, raw_airtable_table, raw_airtable_column, config, import_report
): ):
type_options = raw_airtable_column.get("typeOptions", {}) type_options = raw_airtable_column.get("typeOptions", {})
decimal_places = 0 options_format = type_options.get("format", "")
suffix = ""
if type_options.get("format", "integer") == "decimal": if "percent" in options_format:
# Minimum of 1 and maximum of 5 decimal places. suffix = "%"
decimal_places = min(
max(1, type_options.get("precision", 1)), NUMBER_MAX_DECIMAL_PLACES 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( return NumberField(
number_decimal_places=decimal_places, number_decimal_places=decimal_places,
number_negative=type_options.get("negative", True), number_negative=type_options.get("negative", True),
number_prefix=prefix,
number_suffix=suffix,
number_separator=number_separator,
) )
def to_baserow_export_serialized_value( def to_baserow_export_serialized_value(
@ -180,13 +201,38 @@ class NumberAirtableColumnType(AirtableColumnType):
config, config,
import_report, import_report,
): ):
if value is not None: if value is None:
return None
try:
value = Decimal(value) 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: # Airtable stores 10% as 0.1, so we would need to multiply it by 100 so get the
value = None # 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): class RatingAirtableColumnType(AirtableColumnType):

View file

@ -13,3 +13,9 @@ AIRTABLE_BASEROW_COLOR_MAPPING = {
"purple": "dark-blue", "purple": "dark-blue",
"gray": "light-gray", "gray": "light-gray",
} }
AIRTABLE_NUMBER_FIELD_SEPARATOR_FORMAT_MAPPING = {
"commaPeriod": "COMMA_PERIOD",
"periodComma": "PERIOD_COMMA",
"spaceComma": "SPACE_COMMA",
"spacePeriod": "SPACE_PERIOD",
}

View file

@ -1196,6 +1196,9 @@ def test_airtable_import_number_integer_column(data_fixture, api_client):
assert isinstance(airtable_column_type, NumberAirtableColumnType) assert isinstance(airtable_column_type, NumberAirtableColumnType)
assert baserow_field.number_decimal_places == 0 assert baserow_field.number_decimal_places == 0
assert baserow_field.number_negative is False assert baserow_field.number_negative is False
assert baserow_field.number_separator == ""
assert baserow_field.number_prefix == ""
assert baserow_field.number_suffix == ""
assert ( assert (
airtable_column_type.to_baserow_export_serialized_value( 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 @pytest.mark.django_db
@responses.activate @responses.activate
def test_airtable_import_number_decimal_column(data_fixture, api_client): 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(baserow_field, NumberField)
assert isinstance(airtable_column_type, NumberAirtableColumnType) 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 assert baserow_field.number_negative is False
airtable_field = { airtable_field = {
@ -1319,6 +1366,9 @@ def test_airtable_import_number_decimal_column(data_fixture, api_client):
assert isinstance(airtable_column_type, NumberAirtableColumnType) assert isinstance(airtable_column_type, NumberAirtableColumnType)
assert baserow_field.number_decimal_places == 2 assert baserow_field.number_decimal_places == 2
assert baserow_field.number_negative is True assert baserow_field.number_negative is True
assert baserow_field.number_separator == ""
assert baserow_field.number_prefix == ""
assert baserow_field.number_suffix == ""
assert ( assert (
airtable_column_type.to_baserow_export_serialized_value( 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 isinstance(airtable_column_type, NumberAirtableColumnType)
assert baserow_field.number_decimal_places == 10 assert baserow_field.number_decimal_places == 10
assert baserow_field.number_negative is True 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 @pytest.mark.django_db

View file

@ -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"
}

View file

@ -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"
}