1
0
Fork 0
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:
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 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):

View file

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

View file

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

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