Compare commits

..

126 Commits

Author SHA1 Message Date
MaysWind 2b90ced103 fix cannot delete accounts / transaction categories and tags when using postgres db (#218) 2025-09-03 19:27:29 +08:00
MaysWind 3a4b44762f bump version to 1.0.1 2025-09-03 19:27:03 +08:00
MaysWind 20e2444307 modify color and background image in desktop version 2025-08-31 00:20:17 +08:00
MaysWind 8154bd712b check if the accounts, categories, and tags used exist when creating a transaction template 2025-08-30 01:22:40 +08:00
MaysWind 4d0e376568 code refactor 2025-08-30 00:40:33 +08:00
MaysWind 32cf41a7a0 modify style 2025-08-30 00:07:43 +08:00
MaysWind e85a4701ed fix cannot selecting time when the number system was not Arabic numerals 2025-08-29 00:53:23 +08:00
MaysWind b79ffafaee code refactor 2025-08-29 00:43:34 +08:00
MaysWind 8f6adaa417 fix some numerals were not displayed according to the numerical system 2025-08-29 00:43:11 +08:00
MaysWind 0e634d83f4 update README.md 2025-08-28 23:59:18 +08:00
MaysWind af8cbe0b15 fix the ellipsis was not displayed when the text was too long 2025-08-28 22:43:25 +08:00
MaysWind 411130db4e support calendar display type (Gregorian and Buddhist) 2025-08-28 00:31:59 +08:00
MaysWind c099443783 support date display type (Gregorian and Buddhist) 2025-08-27 00:58:22 +08:00
MaysWind 23ffdbb163 replacing third-party datetime formatter with internal formatter 2025-08-25 23:33:49 +08:00
MaysWind 0b48502a10 modify the style of the custom time range 2025-08-25 22:21:57 +08:00
MaysWind 25681f622d add explicit type for string-based datetimes, replacing third-party datetime type with internal DateTime type 2025-08-25 00:39:36 +08:00
MaysWind f196ce969b code refactor 2025-08-24 23:07:18 +08:00
MaysWind 0408c470fc fix the select dropdown menu was positioned incorrectly after filter box being focused 2025-08-24 23:06:58 +08:00
MaysWind 01aeb945ff check whether transaction template uses specified accounts / categories / tags when deleting them 2025-08-24 01:29:26 +08:00
MaysWind 601a1f83c6 fix the date range may be incorrect when switching between fiscal years 2025-08-24 00:23:49 +08:00
MaysWind 2a470742e0 change order 2025-08-24 00:08:34 +08:00
MaysWind 8ba1e1997f show the time range below the fiscal year option 2025-08-24 00:04:36 +08:00
MaysWind 27ae401a7f code refactor 2025-08-23 23:21:02 +08:00
MaysWind 81727d3b1e show import file type by categories 2025-08-23 02:25:24 +08:00
MaysWind 06a0501633 append error message to log 2025-08-22 21:48:35 +08:00
MaysWind 781c2d9044 retry up to 3 times when update user cloud settings 2025-08-22 00:08:08 +08:00
MaysWind 15e4ad00ee update description 2025-08-21 22:20:56 +08:00
MaysWind 8064a00252 fix confirm dialog color not taking effect 2025-08-21 00:03:25 +08:00
MaysWind f2d0fe407b support deleting all transactions (#202) 2025-08-21 00:01:25 +08:00
MaysWind 9589657fd5 update description 2025-08-20 22:55:16 +08:00
MaysWind 790837076f code refactor 2025-08-20 22:39:49 +08:00
MaysWind 6d923027a0 update README.md 2025-08-20 22:19:11 +08:00
MaysWind 13d5759e84 mobile version supports rtl 2025-08-20 01:13:39 +08:00
MaysWind efe39c7390 fix circular dependency problem 2025-08-18 01:30:58 +08:00
MaysWind c00770201b desktop version supports rtl 2025-08-18 00:45:26 +08:00
MaysWind 4eff3a337f fix repeated execution of multilingual processing 2025-08-17 02:09:09 +08:00
MaysWind 451385011e fix the default date was incorrect when updating the closing balance in the reconciliation statement dialog which the date range is all 2025-08-17 02:04:28 +08:00
MaysWind cd4d230d29 support changing numeral system 2025-08-17 01:55:19 +08:00
MaysWind ab6d4ee6fc code refactor 2025-08-16 01:07:27 +08:00
MaysWind 274aa6a17c digit grouping type supports Indian Number Grouping 2025-08-16 00:20:12 +08:00
MaysWind 2f8d4ad5e4 add new translation contributor 2025-08-13 21:57:04 +08:00
MaysWind fe59d3b280 update transaction 2025-08-13 09:52:32 +08:00
automagics e2c99c4f04 Add Dutch translation 2025-08-13 09:24:27 +08:00
MaysWind 127393b64a display the "Outstanding Balance" label instead of "Balance" for liability account category in desktop account list page 2025-08-10 17:10:59 +08:00
MaysWind f3d240442b modify balance modification transaction 2025-08-10 17:00:08 +08:00
MaysWind 55bf8b9e30 support showing transaction detail dialog / page 2025-08-10 16:59:08 +08:00
MaysWind eadcf7768f fix the total amount of all sub-accounts under the parent account was not counted after filtering any sub-accounts (#192) 2025-08-10 11:26:27 +08:00
MaysWind 876bf8cc31 update dark theme style of desktop version 2025-08-10 01:38:08 +08:00
MaysWind 6b5aac0111 fix the filter box could not be input 2025-08-09 02:13:06 +08:00
MaysWind dc4a4e1463 import latest wechat pay billing file format 2025-08-08 20:37:50 +08:00
MaysWind 0677ed07db code refactor 2025-08-08 20:19:02 +08:00
MaysWind ecf6fbd187 support setting whether the data table in csv / xls / xlsx files contains a header row 2025-08-08 20:15:11 +08:00
MaysWind 351cebe169 add sub basic data table 2025-08-08 20:05:39 +08:00
MaysWind 0f94a90882 add unit tests 2025-08-08 20:05:26 +08:00
MaysWind 04996d784f show the reason why the category / account select is disabled in desktop version 2025-08-06 00:05:35 +08:00
MaysWind aafcfeda84 upgrade node.js to 22.18.0 2025-08-05 23:37:22 +08:00
MaysWind 7283b724b1 add candlestick chart for account balance trends 2025-08-05 23:29:49 +08:00
MaysWind 0d55912f6c modify UI interaction for mode switching 2025-08-05 01:01:52 +08:00
MaysWind 60108e26c7 fix incorrect fiscal year closing balance for partial-year queries 2025-08-05 01:01:33 +08:00
MaysWind be129cd3c6 modify function name 2025-08-04 23:33:13 +08:00
MaysWind f210bfa9f4 use the first visible account as default if the default account is hidden when creating new transaction 2025-08-04 23:14:46 +08:00
MaysWind 263113a67f upgrade third party dependencies 2025-08-04 22:44:41 +08:00
MaysWind 3b29303237 show the reason why the transaction, account, and category cannot be saved on the save button in desktop version 2025-08-04 21:43:19 +08:00
MaysWind 6e5f857e97 show account outstanding balance for liability account in account balance trends chart 2025-08-04 21:04:41 +08:00
MaysWind 791c0ea26e modify style 2025-08-04 21:03:37 +08:00
MaysWind 84523d8b8a code refactor 2025-08-04 20:54:10 +08:00
MaysWind d35e127b9e code refactor 2025-08-04 01:25:55 +08:00
MaysWind ebe00d3271 code refactor 2025-08-04 01:22:54 +08:00
MaysWind 14b4e40039 reconciliation statement page / dialog supports account balance trends chart (#184) 2025-08-04 01:22:36 +08:00
MaysWind 15d1d269ae modify file name 2025-08-03 21:42:30 +08:00
MaysWind e90b76c80e code refactor 2025-08-03 14:27:34 +08:00
MaysWind e28e27080a fix the bug the default account shows "unspecified" in desktop version after the default account is set to hidden 2025-08-03 01:16:51 +08:00
MaysWind 2268496dcb translate text in vuetify controls 2025-08-03 00:58:49 +08:00
MaysWind 3781327c58 hide hidden sub-account 2025-08-02 23:53:11 +08:00
MaysWind 51c33d7e83 upgrade golang to 1.24.5, node.js to 22.17.0, alpine base image to 3.22.1 2025-08-02 10:45:00 +08:00
MaysWind 975a56e7d9 don't initialize avatar / transaction storage when they are not enabled 2025-08-02 10:16:00 +08:00
MaysWind 29a87dcfaf object storage supports webdav 2025-08-02 01:26:29 +08:00
MaysWind cad53d0bfc use the request context 2025-08-02 00:10:12 +08:00
MaysWind 56a3905df1 exclude deleted account 2025-08-01 23:21:37 +08:00
MaysWind 428a1f2156 show "data is up to date" when the data is not updated in reconciliation statement page / dialog 2025-08-01 23:13:03 +08:00
MaysWind b5233399e6 code refactor 2025-08-01 23:03:59 +08:00
MaysWind f8878c5405 disable transaction draft when update closing balance 2025-07-31 23:41:52 +08:00
MaysWind 8dcaa457f9 set the default date based on the current date time filter range when adding a new transaction in the transaction list page 2025-07-31 22:53:37 +08:00
MaysWind b24ebdb83e redesign time picker in date time select for desktop device 2025-07-31 22:19:12 +08:00
MaysWind d41a2141a7 modify style 2025-07-31 14:35:52 +08:00
MaysWind 09a1dd0358 update closing balance in reconciliation statement page 2025-07-30 23:07:04 +08:00
MaysWind 531c4a44d5 code refactor 2025-07-30 22:53:43 +08:00
MaysWind ceecff8c24 add refresh button in reconciliation statement dialog 2025-07-30 22:27:58 +08:00
MaysWind f32cc4ab04 fix the filter could not be saved after the filtered accounts, categories and transaction tags were deleted (#185) 2025-07-30 21:12:15 +08:00
MaysWind 8fa46281e0 update closing balance in reconciliation statement page / dialog 2025-07-30 00:56:55 +08:00
MaysWind f7bc4b3ab6 modify style 2025-07-30 00:56:34 +08:00
MaysWind ad4f5bd88d support duplicating / modifying / deleting transaction in reconciliation statement page 2025-07-29 00:58:15 +08:00
MaysWind e4cb66718d reload reconciliation statement after adding new transaction 2025-07-29 00:16:37 +08:00
MaysWind 175b272fa0 disable the view button of the balance modification transaction 2025-07-29 00:03:45 +08:00
MaysWind ca0fb9446b total inflows and outflows in reconciliation statement includes the amount of the balance modification transaction 2025-07-29 00:01:38 +08:00
MaysWind 6eb749dca2 move file 2025-07-28 23:20:45 +08:00
MaysWind 880b614636 clear the data of the last unfinished calculation when opening the number pad sheet 2025-07-28 23:17:39 +08:00
MaysWind d146a99c65 modify style 2025-07-28 22:49:15 +08:00
MaysWind fd99c784b3 modify style 2025-07-28 22:00:26 +08:00
MaysWind 22f9c5243a add reconciliation statement page for mobile version 2025-07-28 00:37:51 +08:00
MaysWind 67f5aaa5ee update README.md 2025-07-27 23:51:31 +08:00
MaysWind 713b621169 update category display name for balance modification transaction 2025-07-27 21:35:16 +08:00
MaysWind 80df5f95aa fix typo 2025-07-27 17:07:04 +08:00
MaysWind 1e492d8724 code refactor 2025-07-27 16:59:31 +08:00
MaysWind 602f15fe2e export reconciliation statements 2025-07-26 00:58:38 +08:00
MaysWind 3335533a18 remove token via cli 2025-07-25 00:01:05 +08:00
MaysWind d385358aa3 code refactor 2025-07-24 23:58:24 +08:00
MaysWind d6ee8a416f code refactor 2025-07-24 23:57:49 +08:00
MaysWind c5aa37037f the number of digits shown for hours, minutes, and seconds in the time picker depends on the user's language settings 2025-07-23 01:06:34 +08:00
MaysWind 6050f5deab add / view transaction in reconciliation statement dialog 2025-07-23 00:47:22 +08:00
MaysWind 5d07d1a70d modify style 2025-07-23 00:09:15 +08:00
MaysWind bae330c6f3 modify text 2025-07-23 00:02:34 +08:00
MaysWind ea17994c6c show opening / closing balance in reconciliation statement dialog 2025-07-22 01:04:29 +08:00
MaysWind c3d29ee2f8 modify text 2025-07-21 00:53:39 +08:00
MaysWind 515b9af61a add reconciliation statement in desktop version 2025-07-21 00:40:02 +08:00
MaysWind 4ba3893b83 remove unused code 2025-07-21 00:39:54 +08:00
MaysWind bcb6c4f419 time selector in mobile version supports loop selection 2025-07-21 00:39:32 +08:00
MaysWind 53f101fb60 fix the bug that amount format could not be automatically detected when importing dsv file 2025-07-20 13:09:26 +08:00
MaysWind 8da4f65048 load / save rules for batch replacing transaction categories / accounts / tags when import transactions 2025-07-20 01:51:20 +08:00
MaysWind 428bcba56e code refactor 2025-07-19 23:27:32 +08:00
MaysWind 68e896d8eb use the original transaction type of Firefly III as the imported transaction type 2025-07-15 23:13:49 +08:00
MaysWind eef62722a4 update third party dependency copyright 2025-07-13 23:59:13 +08:00
MaysWind e3dcb2ce0c fix wrong link 2025-07-13 23:17:24 +08:00
MaysWind 0cf89562cd use a unified commit hash length when building in different platform 2025-07-13 21:51:14 +08:00
MaysWind 8b06731cdb update go.mod 2025-07-13 17:19:34 +08:00
MaysWind 36abd1acec bump version to 1.0.0 2025-07-13 17:19:27 +08:00
296 changed files with 18150 additions and 6664 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file
FROM golang:1.24.4-alpine3.22 AS be-builder
FROM golang:1.24.5-alpine3.22 AS be-builder
ARG RELEASE_BUILD
ARG BUILD_PIPELINE
ARG CHECK_3RD_API
@@ -15,7 +15,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend
# Build frontend files
FROM --platform=$BUILDPLATFORM node:22.16.0-alpine3.22 AS fe-builder
FROM --platform=$BUILDPLATFORM node:22.18.0-alpine3.22 AS fe-builder
ARG RELEASE_BUILD
ARG BUILD_PIPELINE
ENV RELEASE_BUILD=$RELEASE_BUILD
@@ -27,7 +27,7 @@ RUN apk add git
RUN ./build.sh frontend
# Package docker image
FROM alpine:3.22.0
FROM alpine:3.22.1
LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata
+10 -6
View File
@@ -2,13 +2,16 @@
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/mayswind/ezbookkeeping/blob/master/LICENSE)
[![Latest Build](https://img.shields.io/github/actions/workflow/status/mayswind/ezbookkeeping/docker-snapshot.yml?branch=main)](https://github.com/mayswind/ezbookkeeping/actions)
[![Go Report](https://goreportcard.com/badge/github.com/mayswind/ezbookkeeping)](https://goreportcard.com/report/github.com/mayswind/ezbookkeeping)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mayswind/ezbookkeeping)
[![Latest Docker Image Size](https://img.shields.io/docker/image-size/mayswind/ezbookkeeping.svg?style=flat)](https://hub.docker.com/r/mayswind/ezbookkeeping)
[![Latest Release](https://img.shields.io/github/release/mayswind/ezbookkeeping.svg?style=flat)](https://github.com/mayswind/ezbookkeeping/releases)
## Introduction
ezBookkeeping is a lightweight, self-hosted personal finance app with a sleek, user-friendly interface and powerful bookkeeping features. Built with simplicity and portability in mind, it's easy to deploy, easy to use, and requires minimal system resources — perfect for microservers, NAS devices, and even Raspberry Pi.
[![Recommend By HelloGitHub](https://api.hellogithub.com/v1/widgets/recommend.svg?rid=ded5af09da574ec1811ddb154f1b2093&claim_uid=LT7EZxeBukCnh0K)](https://hellogithub.com/en/repository/mayswind/ezbookkeeping)
The app is fully cross-platform and device-friendly — you can use it seamlessly on **mobile, tablet, and desktop devices**. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app.
## Introduction
ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It's easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient and highly scalable, it can run smoothly on devices as small as a Raspberry Pi, or scale up to NAS, MicroServers, and even large cluster environments.
ezBookkeeping offers tailored interfaces for both mobile and desktop devices. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app.
Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.mayswind.net)
@@ -100,7 +103,7 @@ You can also build a Docker image. Make sure you have [Docker](https://www.docke
$ ./build.sh docker
## Contributing
We welcome contributions of all kinds!
We welcome contributions of all kinds.
Found a bug? [Submit an issue](https://github.com/mayswind/ezbookkeeping/issues)
@@ -111,7 +114,7 @@ Contributions of all kinds — bug reports, feature suggestions, documentation i
Check out our [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors) to see the amazing people whove already helped.
## Translating
Help make ezBookkeeping accessible to users around the world! If you want to contribute a translation, please refer to our [translation guide](https://ezbookkeeping.mayswind.net/translating).
Help make ezBookkeeping accessible to users around the world. If you want to contribute a translation, please refer to our [translation guide](https://ezbookkeeping.mayswind.net/translating).
Currently available translations:
@@ -122,6 +125,7 @@ Currently available translations:
| es | Español | [@Miguelonlonlon](https://github.com/Miguelonlonlon) |
| it | Italiano | [@waron97](https://github.com/waron97) |
| ja | 日本語 | [@tkymmm](https://github.com/tkymmm) |
| nl | Nederlands | [@automagic](https://github.com/automagics) |
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) |
| ru | Русский | [@artegoser](https://github.com/artegoser) |
| uk | Українська | [@nktlitvinenko](https://github.com/nktlitvinenko) |
@@ -129,7 +133,7 @@ Currently available translations:
| zh-Hans | 中文 (简体) | / |
| zh-Hant | 中文 (繁體) | / |
Don't see your language? Help us add it!
Don't see your language? Help us add it.
## Documentation
1. [English](http://ezbookkeeping.mayswind.net)
+1 -1
View File
@@ -112,7 +112,7 @@ goto :pre_parse_args
set VERSION=%VERSION: =%
set VERSION=%VERSION:,=%
set VERSION=%VERSION:"=%
for /f %%x in ('git rev-parse --short HEAD') do set "COMMIT_HASH=%%x"
for /f %%x in ('git rev-parse --short^=7 HEAD') do set "COMMIT_HASH=%%x"
call :set_unixtime BUILD_UNIXTIME
call :set_date BUILD_DATE
+1 -1
View File
@@ -117,7 +117,7 @@ check_type_dependencies() {
set_build_parameters() {
VERSION="$(grep '"version": ' package.json | awk -F ':' '{print $2}' | tr -d ' ' | tr -d ',' | tr -d '"')"
COMMIT_HASH="$(git rev-parse --short HEAD)"
COMMIT_HASH="$(git rev-parse --short=7 HEAD)"
BUILD_UNIXTIME="$(date '+%s')"
}
+4
View File
@@ -158,5 +158,9 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
clonedConfig.SecretKey = "****"
clonedConfig.AmapApplicationSecret = "****"
if clonedConfig.WebDAVConfig != nil {
clonedConfig.WebDAVConfig.Password = "****"
}
return clonedConfig
}
+37 -1
View File
@@ -268,6 +268,19 @@ var UserData = &cli.Command{
},
},
},
{
Name: "user-session-revoke",
Usage: "Revoke the specified user session",
Action: bindAction(revokeUserToken),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Required: false,
Usage: "Specific token content",
},
},
},
{
Name: "user-session-clear",
Usage: "Clear user all sessions",
@@ -732,6 +745,26 @@ func createNewUserToken(c *core.CliContext) error {
return nil
}
func revokeUserToken(c *core.CliContext) error {
_, err := initializeSystem(c)
if err != nil {
return err
}
token := c.String("token")
err = clis.UserData.RevokeUserToken(c, token)
if err != nil {
log.CliErrorf(c, "[user_data.revokeUserToken] error occurs when revoking user token")
return err
}
log.CliInfof(c, "[user_data.revokeUserToken] the specified user token has been revoked successfully")
return nil
}
func clearUserTokens(c *core.CliContext) error {
_, err := initializeSystem(c)
@@ -913,15 +946,18 @@ func printUserInfo(user *models.User) {
fmt.Printf("[DefaultCurrency] %s\n", user.DefaultCurrency)
fmt.Printf("[FirstDayOfWeek] %s (%d)\n", user.FirstDayOfWeek, user.FirstDayOfWeek)
fmt.Printf("[FiscalYearStart] %s (%d)\n", user.FiscalYearStart, user.FiscalYearStart)
fmt.Printf("[CalendarDisplayType] %s (%d)\n", user.CalendarDisplayType, user.CalendarDisplayType)
fmt.Printf("[DateDisplayType] %s (%d)\n", user.DateDisplayType, user.DateDisplayType)
fmt.Printf("[LongDateFormat] %s (%d)\n", user.LongDateFormat, user.LongDateFormat)
fmt.Printf("[ShortDateFormat] %s (%d)\n", user.ShortDateFormat, user.ShortDateFormat)
fmt.Printf("[LongTimeFormat] %s (%d)\n", user.LongTimeFormat, user.LongTimeFormat)
fmt.Printf("[ShortTimeFormat] %s (%d)\n", user.ShortTimeFormat, user.ShortTimeFormat)
fmt.Printf("[FiscalYearFormat] %s (%d)\n", user.FiscalYearFormat, user.FiscalYearFormat)
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
fmt.Printf("[NumeralSystem] %s (%d)\n", user.NumeralSystem, user.NumeralSystem)
fmt.Printf("[DecimalSeparator] %s (%d)\n", user.DecimalSeparator, user.DecimalSeparator)
fmt.Printf("[DigitGroupingSymbol] %s (%d)\n", user.DigitGroupingSymbol, user.DigitGroupingSymbol)
fmt.Printf("[DigitGrouping] %s (%d)\n", user.DigitGrouping, user.DigitGrouping)
fmt.Printf("[CurrencyDisplayType] %s (%d)\n", user.CurrencyDisplayType, user.CurrencyDisplayType)
fmt.Printf("[CoordinateDisplayType] %s (%d)\n", user.CoordinateDisplayType, user.CoordinateDisplayType)
fmt.Printf("[ExpenseAmountColor] %s (%d)\n", user.ExpenseAmountColor, user.ExpenseAmountColor)
fmt.Printf("[IncomeAmountColor] %s (%d)\n", user.IncomeAmountColor, user.IncomeAmountColor)
+2 -2
View File
@@ -81,13 +81,13 @@ func sendTestMail(c *core.CliContext) error {
return err
}
if !config.EnableSMTP || mail.Container.Current == nil {
if !config.EnableSMTP {
return errs.ErrSMTPServerNotEnabled
}
toAddress := c.String("to")
err = mail.Container.Current.SendMail(&mail.MailMessage{
err = mail.Container.SendMail(&mail.MailMessage{
To: toAddress,
Subject: "ezBookkeeping test e-mail",
Body: "This is a test e-mail",
+4 -2
View File
@@ -79,7 +79,7 @@ func startWebServer(c *core.CliContext) error {
return err
}
serverInfo := fmt.Sprintf("current server id is %d, current instance id is %d", requestid.Container.Current.GetCurrentServerUniqId(), requestid.Container.Current.GetCurrentInstanceUniqId())
serverInfo := fmt.Sprintf("current server id is %d, current instance id is %d", requestid.Container.GetCurrentServerUniqId(), requestid.Container.GetCurrentInstanceUniqId())
uuidServerInfo := ""
if config.UuidGeneratorType == settings.InternalUuidGeneratorType {
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
@@ -323,7 +323,8 @@ func startWebServer(c *core.CliContext) error {
// Data
apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler))
apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler))
apiV1Route.POST("/data/clear/all.json", bindApi(api.DataManagements.ClearAllDataHandler))
apiV1Route.POST("/data/clear/transactions.json", bindApi(api.DataManagements.ClearAllTransactionsHandler))
if config.EnableDataExport {
apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler))
@@ -344,6 +345,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler))
apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler))
apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler))
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
+23 -1
View File
@@ -115,7 +115,7 @@ log_file_max_size = 104857600
log_file_max_days = 7
[storage]
# Object storage type, supports "local_filesystem" and "minio" currently
# Object storage type, supports "local_filesystem", "minio" and "webdav" currently
type = local_filesystem
# For "local_filesystem" storage only, the storage root path (relative or absolute path)
@@ -139,6 +139,28 @@ minio_bucket = ezbookkeeping
# For "minio" storage only, the root path to store files in minio
minio_root_path = /
# For "webdav" storage only, the webdav url
webdav_url =
# For "webdav" storage only, the webdav username
webdav_username =
# For "webdav" storage only, the webdav password
webdav_password =
# For "webdav" storage only, the webdav root path to store files
webdav_root_path = /
# For "webdav" storage only, requesting webdav url timeout (0 - 4294967295 milliseconds)
# Set to 0 to disable timeout for requesting webdav url, default is 10000 (10 seconds)
webdav_request_timeout = 10000
# For "webdav" storage only, proxy for requesting webdav url, supports "system" (use system proxy), "none" (do not use proxy), or proxy URL which starts with "http://", "https://" or "socks5://", default is "system"
webdav_proxy = system
# For "webdav" storage only, set to true to skip tls verification when connect webdav
webdav_skip_tls_verify = false
[uuid]
# Uuid generator type, supports "internal" currently
generator_type = internal
+1 -1
View File
@@ -32,7 +32,7 @@ func main() {
cmd := &cli.Command{
Name: "ezBookkeeping",
Usage: "A lightweight, self-hosted personal finance app with a sleek, user-friendly interface and powerful bookkeeping features.",
Usage: "A lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features.",
Version: GetFullVersion(),
Commands: []*cli.Command{
cmd.WebServer,
+21 -21
View File
@@ -3,32 +3,33 @@ module github.com/mayswind/ezbookkeeping
go 1.24
require (
github.com/boombuler/barcode v1.0.2
github.com/boombuler/barcode v1.1.0
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b
github.com/gin-contrib/cache v1.4.0
github.com/gin-contrib/cache v1.4.1
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.1
github.com/go-co-op/gocron/v2 v2.16.2
github.com/go-playground/validator/v10 v10.26.0
github.com/go-sql-driver/mysql v1.9.2
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/go-co-op/gocron/v2 v2.16.3
github.com/go-playground/validator/v10 v10.27.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/invopop/jsonschema v0.13.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.28
github.com/minio/minio-go/v7 v7.0.92
github.com/mattn/go-sqlite3 v1.14.30
github.com/minio/minio-go/v7 v7.0.95
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v3 v3.3.3
github.com/urfave/cli/v3 v3.3.8
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xuri/excelize/v2 v2.9.0
golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
golang.org/x/text v0.25.0
golang.org/x/crypto v0.40.0
golang.org/x/net v0.42.0
golang.org/x/text v0.27.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
xorm.io/xorm v1.3.10
)
require (
@@ -36,7 +37,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
@@ -57,22 +58,21 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/crc64nvme v1.0.2 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
@@ -85,13 +85,13 @@ require (
github.com/tiendc/go-deepcopy v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.2.14 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+40 -40
View File
@@ -4,16 +4,16 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0p
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
@@ -42,30 +42,30 @@ github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cache v1.4.0 h1:d1FUqCE2+gJQKT0vJjr7jMn1htW9+cypk5oF7aoQcmE=
github.com/gin-contrib/cache v1.4.0/go.mod h1:6d0UAPedInkublPl/uJUB4bqwsEgJI1y5QGszhqnyxg=
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@@ -87,8 +87,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -101,16 +101,16 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw=
github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -125,8 +125,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
@@ -164,10 +164,10 @@ github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
@@ -178,23 +178,23 @@ github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmji
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
@@ -215,5 +215,5 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
xorm.io/xorm v1.3.10 h1:yR83hTT4mKIPyA/lvWFTzS35xjLwkiYnwdw0Qupeh0o=
xorm.io/xorm v1.3.10/go.mod h1:Lo7hmsFF0F0GbDE7ubX5ZKa+eCf0eCuiJAUG3oI5cxQ=
+821 -831
View File
File diff suppressed because it is too large Load Diff
+14 -9
View File
@@ -1,6 +1,6 @@
{
"name": "ezbookkeeping",
"version": "0.10.0",
"version": "1.0.1",
"private": true,
"repository": {
"type": "git",
@@ -21,12 +21,12 @@
"dependencies": {
"@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^11.0.2",
"axios": "^1.9.0",
"axios": "^1.11.0",
"cbor-js": "^0.1.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dom7": "^4.0.6",
"echarts": "^5.6.0",
"echarts": "^5.5.1",
"framework7": "^8.3.4",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.4",
@@ -34,18 +34,18 @@
"line-awesome": "^1.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"pinia": "^3.0.2",
"pinia": "^3.0.3",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^10.2.0",
"ua-parser-js": "^1.0.39",
"vue": "^3.5.16",
"vue": "^3.5.18",
"vue-echarts": "^7.0.3",
"vue-i18n": "^11.1.5",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.1",
"vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.8.7"
"vuetify": "^3.9.3"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
@@ -76,8 +76,13 @@
"vue-tsc": "^2.2.10"
},
"browserslist": [
"> 1%",
"last 2 versions",
"last 5 Chrome versions",
"last 5 Firefox versions",
"last 5 Safari versions",
"last 5 Edge versions",
"last 5 ChromeAndroid versions",
"last 5 iOS versions",
"not IE <= 11",
"not dead"
]
}
+10 -10
View File
@@ -23,7 +23,7 @@ type ApiUsingConfig struct {
// CurrentConfig returns the current config
func (a *ApiUsingConfig) CurrentConfig() *settings.Config {
return a.container.Current
return a.container.GetCurrentConfig()
}
// GetTransactionPictureInfoResponse returns the view-object of transaction picture basic info according to the transaction picture model
@@ -53,15 +53,15 @@ func (a *ApiUsingConfig) GetAfterRegisterNotificationContent(userLanguage string
language = clientLanguage
}
if !a.container.Current.AfterRegisterNotification.Enabled {
if !a.CurrentConfig().AfterRegisterNotification.Enabled {
return ""
}
if multiLanguageContent, exists := a.container.Current.AfterRegisterNotification.MultiLanguageContent[language]; exists {
if multiLanguageContent, exists := a.CurrentConfig().AfterRegisterNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return a.container.Current.AfterRegisterNotification.DefaultContent
return a.CurrentConfig().AfterRegisterNotification.DefaultContent
}
// GetAfterLoginNotificationContent returns the notification content displayed each time users log in
@@ -72,15 +72,15 @@ func (a *ApiUsingConfig) GetAfterLoginNotificationContent(userLanguage string, c
language = clientLanguage
}
if !a.container.Current.AfterLoginNotification.Enabled {
if !a.CurrentConfig().AfterLoginNotification.Enabled {
return ""
}
if multiLanguageContent, exists := a.container.Current.AfterLoginNotification.MultiLanguageContent[language]; exists {
if multiLanguageContent, exists := a.CurrentConfig().AfterLoginNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return a.container.Current.AfterLoginNotification.DefaultContent
return a.CurrentConfig().AfterLoginNotification.DefaultContent
}
// GetAfterOpenNotificationContent returns the notification content displayed each time users open the app
@@ -91,15 +91,15 @@ func (a *ApiUsingConfig) GetAfterOpenNotificationContent(userLanguage string, cl
language = clientLanguage
}
if !a.container.Current.AfterOpenNotification.Enabled {
if !a.CurrentConfig().AfterOpenNotification.Enabled {
return ""
}
if multiLanguageContent, exists := a.container.Current.AfterOpenNotification.MultiLanguageContent[language]; exists {
if multiLanguageContent, exists := a.CurrentConfig().AfterOpenNotification.MultiLanguageContent[language]; exists {
return multiLanguageContent
}
return a.container.Current.AfterOpenNotification.DefaultContent
return a.CurrentConfig().AfterOpenNotification.DefaultContent
}
// ApiUsingDuplicateChecker represents an api that need to use duplicate checker
+51 -11
View File
@@ -124,13 +124,13 @@ func (a *DataManagementsApi) DataStatisticsHandler(c *core.WebContext) (any, *er
return dataStatisticsResp, nil
}
// ClearDataHandler deletes all user data
func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Error) {
// ClearAllDataHandler deletes all user data
func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs.Error) {
var clearDataReq models.ClearDataRequest
err := c.ShouldBindJSON(&clearDataReq)
if err != nil {
log.Warnf(c, "[data_managements.ClearDataHandler] parse request failed, because %s", err.Error())
log.Warnf(c, "[data_managements.ClearAllDataHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
@@ -139,7 +139,7 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Er
if err != nil {
if !errs.IsCustomError(err) {
log.Warnf(c, "[data_managements.ClearDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
log.Warnf(c, "[data_managements.ClearAllDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, errs.ErrUserNotFound
@@ -156,39 +156,79 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Er
err = a.templates.DeleteAllTemplates(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction templates, because %s", err.Error())
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transaction templates, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.transactions.DeleteAllTransactions(c, uid)
err = a.transactions.DeleteAllTransactions(c, uid, true)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transactions, because %s", err.Error())
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transactions, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.categories.DeleteAllCategories(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction categories, because %s", err.Error())
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transaction categories, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.tags.DeleteAllTags(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all transaction tags, because %s", err.Error())
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all transaction tags, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.userCustomExchangeRates.DeleteAllCustomExchangeRates(c, uid)
if err != nil {
log.Errorf(c, "[data_managements.ClearDataHandler] failed to delete all user custom exchange rates, because %s", err.Error())
log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all user custom exchange rates, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[data_managements.ClearDataHandler] user \"uid:%d\" has cleared all data", uid)
log.Infof(c, "[data_managements.ClearAllDataHandler] user \"uid:%d\" has cleared all data", uid)
return true, nil
}
// ClearAllTransactionsHandler deletes all transactions
func (a *DataManagementsApi) ClearAllTransactionsHandler(c *core.WebContext) (any, *errs.Error) {
var clearDataReq models.ClearDataRequest
err := c.ShouldBindJSON(&clearDataReq)
if err != nil {
log.Warnf(c, "[data_managements.ClearAllTransactionsHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Warnf(c, "[data_managements.ClearAllTransactionsHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(clearDataReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA) {
return nil, errs.ErrNotPermittedToPerformThisAction
}
err = a.transactions.DeleteAllTransactions(c, uid, false)
if err != nil {
log.Errorf(c, "[data_managements.ClearAllTransactionsHandler] failed to delete all transactions, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.Infof(c, "[data_managements.ClearAllTransactionsHandler] user \"uid:%d\" has cleared all transactions", uid)
return true, nil
}
+1 -7
View File
@@ -30,13 +30,7 @@ var (
// LatestExchangeRateHandler returns latest exchange rate data
func (a *ExchangeRatesApi) LatestExchangeRateHandler(c *core.WebContext) (any, *errs.Error) {
dataSource := exchangerates.Container.Current
if dataSource == nil {
return nil, errs.ErrInvalidExchangeRatesDataSource
}
exchangeRateResponse, err := dataSource.GetLatestExchangeRates(c, c.GetCurrentUid(), a.container.Current)
exchangeRateResponse, err := exchangerates.Container.GetLatestExchangeRates(c, c.GetCurrentUid(), a.CurrentConfig())
if err != nil {
return nil, errs.Or(err, errs.ErrOperationFailed)
+7 -1
View File
@@ -130,7 +130,13 @@ func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Erro
// TokenRevokeCurrentHandler revokes current token of current user
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Error) {
_, claims, err := a.tokens.ParseTokenByHeader(c)
tokenString := c.GetTokenStringFromHeader()
if tokenString == "" {
return false, errs.ErrTokenIsEmpty
}
_, claims, err := a.tokens.ParseToken(c, tokenString)
if err != nil {
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
+120 -2
View File
@@ -22,6 +22,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const pageCountForAccountStatement = 1000
// TransactionsApi represents transaction api
type TransactionsApi struct {
ApiUsingConfig
@@ -286,6 +288,114 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return transactionResps, nil
}
// TransactionReconciliationStatementHandler returns transaction reconciliation statement list of current user
func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebContext) (any, *errs.Error) {
var reconciliationStatementRequest models.TransactionReconciliationStatementRequest
err := c.ShouldBindQuery(&reconciliationStatementRequest)
if err != nil {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
utcOffset, err := c.GetClientTimezoneOffset()
if err != nil {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] cannot get client timezone offset, because %s", err.Error())
return nil, errs.ErrClientTimezoneOffsetInvalid
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
account, err := a.accounts.GetAccountByAccountId(c, uid, reconciliationStatementRequest.AccountId)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get account \"id:%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.AccountId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if account.Type != models.ACCOUNT_TYPE_SINGLE_ACCOUNT {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] account \"id:%d\" for user \"uid:%d\" is not a single account", reconciliationStatementRequest.AccountId, uid)
return nil, errs.ErrAccountTypeInvalid
}
maxTransactionTime := int64(0)
if reconciliationStatementRequest.EndTime > 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(reconciliationStatementRequest.EndTime)
}
minTransactionTime := int64(0)
if reconciliationStatementRequest.StartTime > 0 {
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime)
}
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions := make([]*models.Transaction, len(transactionsWithAccountBalance))
transactionAccountBalanceMap := make(map[int64]*models.TransactionWithAccountBalance, len(transactionsWithAccountBalance))
for i := 0; i < len(transactionsWithAccountBalance); i++ {
transactionWithBalance := transactionsWithAccountBalance[i]
transactions[i] = transactionWithBalance.Transaction
transactionAccountBalanceMap[transactionWithBalance.TransactionId] = transactionWithBalance
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, utcOffset, false, true, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
responseItems := make([]*models.TransactionReconciliationStatementResponseItem, len(transactionResult))
for i := 0; i < len(transactionResult); i++ {
transactionResult := transactionResult[i]
accountOpeningBalance := int64(0)
accountClosingBalance := int64(0)
if transactionWithBalance, exists := transactionAccountBalanceMap[transactionResult.Id]; exists {
accountOpeningBalance = transactionWithBalance.AccountOpeningBalance
accountClosingBalance = transactionWithBalance.AccountClosingBalance
} else {
log.Warnf(c, "[transactions.TransactionReconciliationStatementHandler] missing account balance for transaction \"id:%d\" of user \"uid:%d\"", transactionResult.Id, uid)
}
responseItems[i] = &models.TransactionReconciliationStatementResponseItem{
TransactionInfoResponse: transactionResult,
AccountOpeningBalance: accountOpeningBalance,
AccountClosingBalance: accountClosingBalance,
}
}
reconciliationStatementResp := &models.TransactionReconciliationStatementResponse{
Transactions: responseItems,
TotalInflows: totalInflows,
TotalOutflows: totalOutflows,
OpeningBalance: openingBalance,
ClosingBalance: closingBalance,
}
return reconciliationStatementResp, nil
}
// TransactionStatisticsHandler returns transaction statistics of current user
func (a *TransactionsApi) TransactionStatisticsHandler(c *core.WebContext) (any, *errs.Error) {
var statisticReq models.TransactionStatisticRequest
@@ -699,7 +809,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
return nil, errs.ErrTransactionTypeInvalid
}
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId > 0 {
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId != 0 {
log.Warnf(c, "[transactions.TransactionCreateHandler] balance modification transaction cannot set category id")
return nil, errs.ErrBalanceModificationTransactionCannotSetCategory
}
@@ -847,6 +957,14 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
return nil, errs.ErrTransactionTypeInvalid
}
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE && transactionModifyReq.CategoryId != 0 {
log.Warnf(c, "[transactions.TransactionModifyHandler] balance modification transaction cannot set category id")
return nil, errs.ErrBalanceModificationTransactionCannotSetCategory
} else if transaction.Type != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE && transactionModifyReq.CategoryId == 0 {
log.Warnf(c, "[transactions.TransactionModifyHandler] non-balance modification transaction must set category id")
return nil, errs.ErrIncompleteOrIncorrectSubmission
}
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, []int64{transaction.TransactionId})
if err != nil {
@@ -1388,7 +1506,7 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
return nil, errs.ErrTransactionTypeInvalid
}
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId > 0 {
if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId != 0 {
log.Warnf(c, "[transactions.TransactionImportHandler] balance modification transaction \"index:%d\" cannot set category id", i)
return nil, errs.ErrBalanceModificationTransactionCannotSetCategory
}
+99 -84
View File
@@ -2,6 +2,7 @@ package api
import (
"encoding/json"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -74,115 +75,129 @@ func (a *UserApplicationCloudSettingsApi) ApplicationSettingsUpdateHandler(c *co
return false, errs.ErrNotPermittedToPerformThisAction
}
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, uid)
var userApplicationCloudSettings *models.UserApplicationCloudSetting
if err != nil {
log.Errorf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", uid, err.Error())
return false, errs.Or(err, errs.ErrOperationFailed)
}
// Retry up to 3 times
for i := 0; i < 3; i++ {
userApplicationCloudSettings, err = a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, uid)
oldApplicationCloudSettingsMap := make(map[string]models.ApplicationCloudSetting)
if userApplicationCloudSettings != nil {
for _, setting := range userApplicationCloudSettings.Settings {
oldApplicationCloudSettingsMap[setting.SettingKey] = setting
if err != nil {
log.Errorf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] failed to get latest user application cloud settings for user \"uid:%d\" (try count %d), because %s", uid, i+1, err.Error())
return false, errs.Or(err, errs.ErrOperationFailed)
}
}
// Check if the full update settings are the same as the existing settings
if userAppCloudSettingUpdateReq.FullUpdate {
if len(userAppCloudSettingUpdateReq.Settings) == len(oldApplicationCloudSettingsMap) {
needUpdate := false
oldApplicationCloudSettingsMap := make(map[string]models.ApplicationCloudSetting)
lastUpdateTime := int64(0)
if userApplicationCloudSettings != nil {
for _, setting := range userApplicationCloudSettings.Settings {
oldApplicationCloudSettingsMap[setting.SettingKey] = setting
}
lastUpdateTime = userApplicationCloudSettings.UpdatedUnixTime
}
// Check if the full update settings are the same as the existing settings
if userAppCloudSettingUpdateReq.FullUpdate {
if len(userAppCloudSettingUpdateReq.Settings) == len(oldApplicationCloudSettingsMap) {
needUpdate := false
for _, setting := range userAppCloudSettingUpdateReq.Settings {
oldSetting, exists := oldApplicationCloudSettingsMap[setting.SettingKey]
if !exists || oldSetting.SettingValue != setting.SettingValue {
needUpdate = true
break
}
}
if !needUpdate {
return false, errs.ErrNothingWillBeUpdated
}
}
} else { // Check if the partial update settings are the same as the existing settings or the settings to update are not set to sync
needUpdate := true
for _, setting := range userAppCloudSettingUpdateReq.Settings {
oldSetting, exists := oldApplicationCloudSettingsMap[setting.SettingKey]
cloudSetting, exists := oldApplicationCloudSettingsMap[setting.SettingKey]
if !exists || oldSetting.SettingValue != setting.SettingValue {
needUpdate = true
break
if !exists {
needUpdate = false
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" is not set to sync (try count %d)", setting.SettingKey, i+1)
} else if cloudSetting.SettingValue == setting.SettingValue {
needUpdate = false
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" value \"%s\" is not changed, no need to update (try count %d)", setting.SettingKey, setting.SettingValue, i+1)
}
}
if !needUpdate {
return false, errs.ErrNothingWillBeUpdated
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] no user application cloud settings need to update for user \"uid:%d\" (try count %d)", uid, i+1)
return true, nil
}
}
newApplicationCloudSettingsMap := make(map[string]models.ApplicationCloudSetting)
var newApplicationCloudSettingSlice models.ApplicationCloudSettingSlice
if userAppCloudSettingUpdateReq.FullUpdate {
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user \"uid:%d\" application cloud settings force update, will overwrite all existing settings (try count %d)", uid, i+1)
} else {
if len(oldApplicationCloudSettingsMap) > 0 {
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user \"uid:%d\" application cloud settings exists, try to merge it with request settings (try count %d)", uid, i+1)
newApplicationCloudSettingsMap = oldApplicationCloudSettingsMap
}
}
} else { // Check if the partial update settings are the same as the existing settings or the settings to update are not set to sync
needUpdate := true
for _, setting := range userAppCloudSettingUpdateReq.Settings {
cloudSetting, exists := oldApplicationCloudSettingsMap[setting.SettingKey]
newApplicationCloudSettingsMap[setting.SettingKey] = setting
}
for settingKey, setting := range newApplicationCloudSettingsMap {
settingType, exists := models.ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES[settingKey]
if !exists {
needUpdate = false
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" is not set to sync", setting.SettingKey)
} else if cloudSetting.SettingValue == setting.SettingValue {
needUpdate = false
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" value \"%s\" is not changed, no need to update", setting.SettingKey, setting.SettingValue)
}
}
if !needUpdate {
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] no user application cloud settings need to update for user \"uid:%d\"", uid)
return false, nil
}
}
newApplicationCloudSettingsMap := make(map[string]models.ApplicationCloudSetting)
var newApplicationCloudSettingSlice models.ApplicationCloudSettingSlice
if userAppCloudSettingUpdateReq.FullUpdate {
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user \"uid:%d\" application cloud settings force update, will overwrite all existing settings", uid)
} else {
if len(oldApplicationCloudSettingsMap) > 0 {
log.Infof(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user \"uid:%d\" application cloud settings exists, try to merge it with request settings", uid)
newApplicationCloudSettingsMap = oldApplicationCloudSettingsMap
}
}
for _, setting := range userAppCloudSettingUpdateReq.Settings {
newApplicationCloudSettingsMap[setting.SettingKey] = setting
}
for settingKey, setting := range newApplicationCloudSettingsMap {
settingType, exists := models.ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES[settingKey]
if !exists {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" is not supported to sync", settingKey)
continue
}
if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_STRING {
// Do Nothing
} else if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER {
_, err := utils.StringToFloat64(setting.SettingValue)
if err != nil {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has invalid number value \"%s\"", settingKey, setting.SettingValue)
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" is not supported to sync (try count %d)", settingKey, i+1)
continue
}
} else if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN {
if setting.SettingValue != "true" && setting.SettingValue != "false" {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has invalid boolean value \"%s\"", settingKey, setting.SettingValue)
continue
}
} else if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_STRING_BOOLEAN_MAP {
var settingValueMap map[string]bool
err := json.Unmarshal([]byte(setting.SettingValue), &settingValueMap)
if err != nil {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has invalid map value \"%s\", because %s", settingKey, setting.SettingValue, err.Error())
if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_STRING {
// Do Nothing
} else if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER {
_, err := utils.StringToFloat64(setting.SettingValue)
if err != nil {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has invalid number value \"%s\" (try count %d)", settingKey, setting.SettingValue, i+1)
continue
}
} else if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN {
if setting.SettingValue != "true" && setting.SettingValue != "false" {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has invalid boolean value \"%s\" (try count %d)", settingKey, setting.SettingValue, i+1)
continue
}
} else if settingType == models.USER_APPLICATION_CLOUD_SETTING_TYPE_STRING_BOOLEAN_MAP {
var settingValueMap map[string]bool
err := json.Unmarshal([]byte(setting.SettingValue), &settingValueMap)
if err != nil {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has invalid map value \"%s\" (try count %d), because %s", settingKey, setting.SettingValue, i+1, err.Error())
continue
}
} else {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has unknown type \"%s\" (try count %d)", settingKey, settingType, i+1)
continue
}
} else {
log.Warnf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] user application cloud setting key \"%s\" has unknown type \"%s\"", settingKey, settingType)
continue
newApplicationCloudSettingSlice = append(newApplicationCloudSettingSlice, setting)
}
newApplicationCloudSettingSlice = append(newApplicationCloudSettingSlice, setting)
}
err = a.userAppCloudSettings.UpdateUserApplicationCloudSettings(c, uid, newApplicationCloudSettingSlice, userAppCloudSettingUpdateReq.FullUpdate, lastUpdateTime)
err = a.userAppCloudSettings.UpdateUserApplicationCloudSettings(c, uid, newApplicationCloudSettingSlice)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond) // Wait for 100 milliseconds before retrying
}
if err != nil {
log.Errorf(c, "[user_app_cloud_settings.ApplicationSettingsUpdateHandler] failed to update user application cloud settings for user \"uid:%d\", because %s", uid, err.Error())
+36 -9
View File
@@ -359,6 +359,24 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
userNew.FiscalYearStart = core.FISCAL_YEAR_START_INVALID
}
if userUpdateReq.CalendarDisplayType != nil && *userUpdateReq.CalendarDisplayType != user.CalendarDisplayType {
user.CalendarDisplayType = *userUpdateReq.CalendarDisplayType
userNew.CalendarDisplayType = *userUpdateReq.CalendarDisplayType
modifyProfileBasicInfo = true
anythingUpdate = true
} else {
userNew.CalendarDisplayType = core.CALENDAR_DISPLAY_TYPE_INVALID
}
if userUpdateReq.DateDisplayType != nil && *userUpdateReq.DateDisplayType != user.DateDisplayType {
user.DateDisplayType = *userUpdateReq.DateDisplayType
userNew.DateDisplayType = *userUpdateReq.DateDisplayType
modifyProfileBasicInfo = true
anythingUpdate = true
} else {
userNew.DateDisplayType = core.DATE_DISPLAY_TYPE_INVALID
}
if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat {
user.LongDateFormat = *userUpdateReq.LongDateFormat
userNew.LongDateFormat = *userUpdateReq.LongDateFormat
@@ -404,6 +422,24 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
userNew.FiscalYearFormat = core.FISCAL_YEAR_FORMAT_INVALID
}
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
modifyProfileBasicInfo = true
anythingUpdate = true
} else {
userNew.CurrencyDisplayType = core.CURRENCY_DISPLAY_TYPE_INVALID
}
if userUpdateReq.NumeralSystem != nil && *userUpdateReq.NumeralSystem != user.NumeralSystem {
user.NumeralSystem = *userUpdateReq.NumeralSystem
userNew.NumeralSystem = *userUpdateReq.NumeralSystem
modifyProfileBasicInfo = true
anythingUpdate = true
} else {
userNew.NumeralSystem = core.NUMERAL_SYSTEM_INVALID
}
if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator {
user.DecimalSeparator = *userUpdateReq.DecimalSeparator
userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator
@@ -431,15 +467,6 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro
userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID
}
if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType {
user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType
modifyProfileBasicInfo = true
anythingUpdate = true
} else {
userNew.CurrencyDisplayType = core.CURRENCY_DISPLAY_TYPE_INVALID
}
if userUpdateReq.CoordinateDisplayType != nil && *userUpdateReq.CoordinateDisplayType != user.CoordinateDisplayType {
user.CoordinateDisplayType = *userUpdateReq.CoordinateDisplayType
userNew.CoordinateDisplayType = *userUpdateReq.CoordinateDisplayType
+9 -5
View File
@@ -9,7 +9,7 @@ import (
// AvatarProviderContainer contains the current user avatar provider
type AvatarProviderContainer struct {
Current AvatarProvider
current AvatarProvider
}
// Initialize a user avatar provider container singleton instance
@@ -20,13 +20,13 @@ var (
// InitializeAvatarProvider initializes the current user avatar provider according to the config
func InitializeAvatarProvider(config *settings.Config) error {
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
Container.Current = NewInternalStorageAvatarProvider(config)
Container.current = NewInternalStorageAvatarProvider(config)
return nil
} else if config.AvatarProvider == core.USER_AVATAR_PROVIDER_GRAVATAR {
Container.Current = NewGravatarAvatarProvider()
Container.current = NewGravatarAvatarProvider()
return nil
} else if config.AvatarProvider == "" {
Container.Current = NewNullAvatarProvider()
Container.current = NewNullAvatarProvider()
return nil
}
@@ -35,5 +35,9 @@ func InitializeAvatarProvider(config *settings.Config) error {
// GetAvatarUrl returns the avatar url by the current user avatar provider
func (p *AvatarProviderContainer) GetAvatarUrl(user *models.User) string {
return p.Current.GetAvatarUrl(user)
if p.current == nil {
return ""
}
return p.current.GetAvatarUrl(user)
}
+1 -1
View File
@@ -9,5 +9,5 @@ type CliUsingConfig struct {
// CurrentConfig returns the current config
func (l *CliUsingConfig) CurrentConfig() *settings.Config {
return l.container.Current
return l.container.GetCurrentConfig()
}
+33
View File
@@ -445,6 +445,39 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
return tokenRecord, token, nil
}
// RevokeUserToken revokes the specified token of the user
func (l *UserDataCli) RevokeUserToken(c *core.CliContext, token string) error {
_, claims, err := l.tokens.ParseToken(c, token)
if err != nil {
log.CliErrorf(c, "[user_data.RevokeUserToken] failed to parse token, because %s", err.Error())
return err
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.CliErrorf(c, "[user_data.RevokeUserToken] failed to get user token id, because %s", err.Error())
return err
}
tokenRecord := &models.TokenRecord{
Uid: claims.Uid,
UserTokenId: userTokenId,
CreatedUnixTime: claims.IssuedAt,
}
tokenId := l.tokens.GenerateTokenId(tokenRecord)
err = l.tokens.DeleteToken(c, tokenRecord)
if err != nil {
log.Errorf(c, "[user_data.RevokeUserToken] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
return err
}
return nil
}
// ClearUserTokens clears all tokens of the specified user
func (l *UserDataCli) ClearUserTokens(c *core.CliContext, username string) error {
if username == "" {
@@ -2,21 +2,17 @@ package alipay
import (
"bytes"
"encoding/csv"
"io"
"strings"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
csvdatatable "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
var alipayTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
@@ -61,7 +57,13 @@ func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Contex
enc := simplifiedchinese.GB18030
reader := transform.NewReader(bytes.NewReader(data), enc.NewDecoder())
dataTable, err := c.createNewAlipayBasicDataTable(ctx, reader, c.fileHeaderLine, c.dataHeaderStartContent, c.dataBottomEndLineRune)
csvDataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, false)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTable, err := createNewAlipayTransactionBasicDataTable(ctx, csvDataTable, c.fileHeaderLine, c.dataHeaderStartContent, c.dataBottomEndLineRune)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
@@ -83,80 +85,3 @@ func (c *alipayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Contex
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
func (c *alipayTransactionDataCsvFileImporter) createNewAlipayBasicDataTable(ctx core.Context, reader io.Reader, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.BasicDataTable, error) {
csvReader := csv.NewReader(reader)
csvReader.FieldsPerRecord = -1
allOriginalLines := make([][]string, 0)
hasFileHeader := false
foundContentBeforeDataHeaderLine := false
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[alipay_transaction_data_csv_file_importer.createNewAlipayBasicDataTable] cannot parse alipay csv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if !hasFileHeader {
if len(items) <= 0 {
continue
} else if strings.Index(items[0], fileHeaderLine) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[alipay_transaction_data_csv_file_importer.createNewAlipayBasicDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ","))
continue
}
}
if !foundContentBeforeDataHeaderLine {
if len(items) <= 0 {
continue
} else if strings.Index(items[0], dataHeaderStartContent) >= 0 {
foundContentBeforeDataHeaderLine = true
continue
} else {
continue
}
}
if foundContentBeforeDataHeaderLine {
if len(items) <= 0 {
continue
} else if len(items) == 1 && dataBottomEndLineRune > 0 && utils.ContainsOnlyOneRune(items[0], dataBottomEndLineRune) {
break
}
for i := 0; i < len(items); i++ {
items[i] = strings.Trim(items[i], " ")
}
if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) {
log.Errorf(ctx, "[alipay_transaction_data_csv_file_importer.createNewAlipayBasicDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0]))
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
allOriginalLines = append(allOriginalLines, items)
}
}
if !hasFileHeader || !foundContentBeforeDataHeaderLine {
return nil, errs.ErrInvalidFileHeader
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[alipay_transaction_data_csv_file_importer.createNewAlipayBasicDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
dataTable := csvdatatable.CreateNewCustomCsvBasicDataTable(allOriginalLines)
return dataTable, nil
}
@@ -0,0 +1,78 @@
package alipay
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func createNewAlipayTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable, fileHeaderLine string, dataHeaderStartContent string, dataBottomEndLineRune rune) (datatable.BasicDataTable, error) {
iterator := originalDataTable.DataRowIterator()
allOriginalLines := make([][]string, 0)
hasFileHeader := false
foundContentBeforeDataHeaderLine := false
for iterator.HasNext() {
row := iterator.Next()
if !hasFileHeader {
if row.ColumnCount() <= 0 {
continue
} else if strings.Index(row.GetData(0), fileHeaderLine) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[alipay_transaction_data_extrator.createNewAlipayTransactionBasicDataTable] read unexpected line in row \"%s\" before read file header", iterator.CurrentRowId())
continue
}
}
if !foundContentBeforeDataHeaderLine {
if row.ColumnCount() <= 0 {
continue
} else if strings.Index(row.GetData(0), dataHeaderStartContent) >= 0 {
foundContentBeforeDataHeaderLine = true
continue
} else {
continue
}
}
if foundContentBeforeDataHeaderLine {
if row.ColumnCount() <= 0 {
continue
} else if row.ColumnCount() == 1 && dataBottomEndLineRune > 0 && utils.ContainsOnlyOneRune(row.GetData(0), dataBottomEndLineRune) {
break
}
items := make([]string, row.ColumnCount())
for i := 0; i < row.ColumnCount(); i++ {
items[i] = strings.Trim(row.GetData(i), " ")
}
if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) {
log.Errorf(ctx, "[alipay_transaction_data_extrator.createNewAlipayTransactionBasicDataTable] cannot parse row \"%s\", because may missing some columns (column count %d in data row is less than header column count %d)", iterator.CurrentRowId(), len(items), len(allOriginalLines[0]))
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
allOriginalLines = append(allOriginalLines, items)
}
}
if !hasFileHeader || !foundContentBeforeDataHeaderLine {
return nil, errs.ErrInvalidFileHeader
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[alipay_transaction_data_extrator.createNewAlipayTransactionBasicDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
return csv.CreateNewCustomCsvBasicDataTable(allOriginalLines, true), nil
}
+27 -10
View File
@@ -13,7 +13,8 @@ import (
// CsvFileBasicDataTable defines the structure of csv data table
type CsvFileBasicDataTable struct {
allLines [][]string
allLines [][]string
hasTitleLine bool
}
// CsvFileBasicDataTableRow defines the structure of csv data table row
@@ -34,7 +35,11 @@ func (t *CsvFileBasicDataTable) DataRowCount() int {
return 0
}
return len(t.allLines) - 1
if t.hasTitleLine {
return len(t.allLines) - 1
} else {
return len(t.allLines)
}
}
// HeaderColumnNames returns the header column name list
@@ -43,14 +48,24 @@ func (t *CsvFileBasicDataTable) HeaderColumnNames() []string {
return nil
}
return t.allLines[0]
if t.hasTitleLine {
return t.allLines[0]
} else {
return nil
}
}
// DataRowIterator returns the iterator of data row
func (t *CsvFileBasicDataTable) DataRowIterator() datatable.BasicDataTableRowIterator {
startIndex := -1
if t.hasTitleLine {
startIndex = 0
}
return &CsvFileBasicDataTableRowIterator{
dataTable: t,
currentIndex: 0,
currentIndex: startIndex,
}
}
@@ -95,18 +110,19 @@ func (t *CsvFileBasicDataTableRowIterator) Next() datatable.BasicDataTableRow {
}
// CreateNewCsvBasicDataTable returns comma separated values data table by io readers
func CreateNewCsvBasicDataTable(ctx core.Context, reader io.Reader) (datatable.BasicDataTable, error) {
return createNewCsvFileBasicDataTable(ctx, reader, ',')
func CreateNewCsvBasicDataTable(ctx core.Context, reader io.Reader, hasTitleLine bool) (datatable.BasicDataTable, error) {
return createNewCsvFileBasicDataTable(ctx, reader, ',', hasTitleLine)
}
// CreateNewCustomCsvBasicDataTable returns character separated values data table by io readers
func CreateNewCustomCsvBasicDataTable(allLines [][]string) datatable.BasicDataTable {
func CreateNewCustomCsvBasicDataTable(allLines [][]string, hasTitleLine bool) datatable.BasicDataTable {
return &CsvFileBasicDataTable{
allLines: allLines,
allLines: allLines,
hasTitleLine: hasTitleLine,
}
}
func createNewCsvFileBasicDataTable(ctx core.Context, reader io.Reader, separator rune) (*CsvFileBasicDataTable, error) {
func createNewCsvFileBasicDataTable(ctx core.Context, reader io.Reader, separator rune, hasTitleLine bool) (*CsvFileBasicDataTable, error) {
csvReader := csv.NewReader(reader)
csvReader.Comma = separator
csvReader.FieldsPerRecord = -1
@@ -133,6 +149,7 @@ func createNewCsvFileBasicDataTable(ctx core.Context, reader io.Reader, separato
}
return &CsvFileBasicDataTable{
allLines: allLines,
allLines: allLines,
hasTitleLine: hasTitleLine,
}, nil
}
@@ -14,7 +14,17 @@ func TestCsvFileBasicDataTableDataRowCount(t *testing.T) {
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
})
}, false)
assert.Equal(t, 3, datatable.DataRowCount())
}
func TestCsvFileBasicDataTableDataRowCount_HasTitleLine(t *testing.T) {
datatable := CreateNewCustomCsvBasicDataTable([][]string{
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
}, true)
assert.Equal(t, 2, datatable.DataRowCount())
}
@@ -22,14 +32,16 @@ func TestCsvFileBasicDataTableDataRowCount(t *testing.T) {
func TestCsvFileBasicDataTableDataRowCount_OnlyHeaderLine(t *testing.T) {
datatable := CreateNewCustomCsvBasicDataTable([][]string{
{"A1", "B1", "C1"},
})
}, true)
assert.Equal(t, 0, datatable.DataRowCount())
}
func TestCsvFileBasicDataTableDataRowCount_EmptyContent(t *testing.T) {
datatable := CreateNewCustomCsvBasicDataTable([][]string{})
datatable := CreateNewCustomCsvBasicDataTable([][]string{}, false)
assert.Equal(t, 0, datatable.DataRowCount())
datatable = CreateNewCustomCsvBasicDataTable([][]string{}, true)
assert.Equal(t, 0, datatable.DataRowCount())
}
@@ -38,14 +50,16 @@ func TestCsvFileBasicDataTableHeaderColumnNames(t *testing.T) {
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
})
}, true)
assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames())
}
func TestCsvFileBasicDataTableHeaderColumnNames_EmptyContent(t *testing.T) {
datatable := CreateNewCustomCsvBasicDataTable([][]string{})
datatable := CreateNewCustomCsvBasicDataTable([][]string{}, false)
assert.Nil(t, datatable.HeaderColumnNames())
datatable = CreateNewCustomCsvBasicDataTable([][]string{}, true)
assert.Nil(t, datatable.HeaderColumnNames())
}
@@ -54,7 +68,34 @@ func TestCsvFileBasicDataTableRowIterator(t *testing.T) {
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
})
}, false)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
// data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// data row 3
assert.NotNil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row 4
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestCsvFileBasicDataTableRowIterator_HasTitleLine(t *testing.T) {
datatable := CreateNewCustomCsvBasicDataTable([][]string{
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
}, true)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
@@ -81,7 +122,7 @@ func TestCsvFileBasicDataTableRowColumnCount(t *testing.T) {
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
})
}, true)
iterator := datatable.DataRowIterator()
@@ -97,7 +138,32 @@ func TestCsvFileBasicDataTableRowGetData(t *testing.T) {
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
})
}, false)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
assert.Equal(t, "A1", row1.GetData(0))
assert.Equal(t, "B1", row1.GetData(1))
assert.Equal(t, "C1", row1.GetData(2))
row2 := iterator.Next()
assert.Equal(t, "A2", row2.GetData(0))
assert.Equal(t, "B2", row2.GetData(1))
assert.Equal(t, "C2", row2.GetData(2))
row3 := iterator.Next()
assert.Equal(t, "A3", row3.GetData(0))
assert.Equal(t, "B3", row3.GetData(1))
assert.Equal(t, "C3", row3.GetData(2))
}
func TestCsvFileBasicDataTableRowGetData_HasTitleLine(t *testing.T) {
datatable := CreateNewCustomCsvBasicDataTable([][]string{
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
}, true)
iterator := datatable.DataRowIterator()
@@ -117,7 +183,7 @@ func TestCsvFileBasicDataTableRowGetData_GetNotExistedColumnData(t *testing.T) {
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
})
}, true)
iterator := datatable.DataRowIterator()
@@ -130,7 +196,7 @@ func TestCreateNewCsvBasicDataTable(t *testing.T) {
reader := bytes.NewReader([]byte("A1,B1,C1\n" +
"A2,B2,C2\n" +
"A3,B3,C3\n"))
datatable, err := CreateNewCsvBasicDataTable(context, reader)
datatable, err := CreateNewCsvBasicDataTable(context, reader, true)
assert.Nil(t, err)
assert.Equal(t, 2, datatable.DataRowCount())
@@ -160,7 +226,7 @@ func TestCreateNewCsvBasicDataTable_SkipBlankLine(t *testing.T) {
"A2,B2,C2\n" +
"\n" +
"A3,B3,C3\n"))
datatable, err := CreateNewCsvBasicDataTable(context, reader)
datatable, err := CreateNewCsvBasicDataTable(context, reader, true)
assert.Nil(t, err)
assert.Equal(t, 2, datatable.DataRowCount())
@@ -0,0 +1,65 @@
package datatable
type testBasicDataTable struct {
headerColumns []string
rows []*testBasicDataTableRow
}
type testBasicDataTableRow struct {
rowId string
rowColumns []string
}
type testBasicDataTableRowIterator struct {
rows []*testBasicDataTableRow
currentIndex int
}
func (t *testBasicDataTable) HeaderColumnNames() []string {
return t.headerColumns
}
func (t *testBasicDataTable) DataRowCount() int {
return len(t.rows)
}
func (t *testBasicDataTable) DataRowIterator() BasicDataTableRowIterator {
return &testBasicDataTableRowIterator{
rows: t.rows,
currentIndex: -1,
}
}
func (r *testBasicDataTableRow) ColumnCount() int {
return len(r.rowColumns)
}
func (r *testBasicDataTableRow) GetData(columnIndex int) string {
if columnIndex < 0 || columnIndex >= len(r.rowColumns) {
return ""
}
return r.rowColumns[columnIndex]
}
func (t *testBasicDataTableRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.rows)
}
func (t *testBasicDataTableRowIterator) CurrentRowId() string {
if t.currentIndex >= len(t.rows) {
return ""
}
return t.rows[t.currentIndex].rowId
}
func (t *testBasicDataTableRowIterator) Next() BasicDataTableRow {
if t.currentIndex+1 >= len(t.rows) {
return nil
}
t.currentIndex++
row := t.rows[t.currentIndex]
return row
}
@@ -0,0 +1,108 @@
package datatable
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBasicDataTableToCommonDataTableWrapper_HeaderColumnCount(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: []*testBasicDataTableRow{},
}
commonDataTable := CreateNewCommonDataTableFromBasicDataTable(basicDataTable)
assert.Equal(t, len(columns), commonDataTable.HeaderColumnCount())
}
func TestBasicDataTableToCommonDataTableWrapper_HasColumn(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: []*testBasicDataTableRow{},
}
commonDataTable := CreateNewCommonDataTableFromBasicDataTable(basicDataTable)
assert.True(t, commonDataTable.HasColumn("Col1"))
assert.True(t, commonDataTable.HasColumn("Col2"))
assert.True(t, commonDataTable.HasColumn("Col3"))
assert.False(t, commonDataTable.HasColumn("Col4"))
assert.False(t, commonDataTable.HasColumn(""))
}
func TestBasicDataTableToCommonDataTableWrapper_DataRowCount(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"A1", "B1", "C1"},
},
{
rowId: "2",
rowColumns: []string{"A2", "B2", "C2"},
},
{
rowId: "3",
rowColumns: []string{"A3", "B3", "C3"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
commonDataTable := CreateNewCommonDataTableFromBasicDataTable(basicDataTable)
assert.Equal(t, len(rows), commonDataTable.DataRowCount())
}
func TestBasicDataTableToCommonDataTableWrapper_DataRowIterator(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"A1", "B1", "C1"},
},
{
rowId: "2",
rowColumns: []string{"A2", "B2", "C2"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
commonDataTable := CreateNewCommonDataTableFromBasicDataTable(basicDataTable)
iterator := commonDataTable.DataRowIterator()
assert.True(t, iterator.HasNext())
firstRow := iterator.Next()
assert.NotNil(t, firstRow)
assert.Equal(t, len(columns), firstRow.ColumnCount())
assert.True(t, firstRow.HasData("Col1"))
assert.True(t, firstRow.HasData("Col2"))
assert.True(t, firstRow.HasData("Col3"))
assert.Equal(t, "A1", firstRow.GetData("Col1"))
assert.Equal(t, "B1", firstRow.GetData("Col2"))
assert.Equal(t, "C1", firstRow.GetData("Col3"))
assert.True(t, iterator.HasNext())
secondRow := iterator.Next()
assert.NotNil(t, secondRow)
assert.Equal(t, len(columns), secondRow.ColumnCount())
assert.True(t, secondRow.HasData("Col1"))
assert.True(t, secondRow.HasData("Col2"))
assert.True(t, secondRow.HasData("Col3"))
assert.Equal(t, "A2", secondRow.GetData("Col1"))
assert.Equal(t, "B2", secondRow.GetData("Col2"))
assert.Equal(t, "C2", secondRow.GetData("Col3"))
assert.False(t, iterator.HasNext())
assert.Nil(t, iterator.Next())
}
@@ -0,0 +1,220 @@
package datatable
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
type testTransactionDataRowParser struct {
}
func (p *testTransactionDataRowParser) Parse(rowData map[TransactionDataTableColumn]string) (map[TransactionDataTableColumn]string, bool, error) {
rowData[TRANSACTION_DATA_TABLE_DESCRIPTION] = "Test Description"
return rowData, true, nil
}
func (p *testTransactionDataRowParser) GetAddedColumns() []TransactionDataTableColumn {
return []TransactionDataTableColumn{TRANSACTION_DATA_TABLE_DESCRIPTION}
}
func TestBasicDataTableToTransactionDataTableWrapper_HasColumn(t *testing.T) {
columns := []string{"TransactionTime", "TransactionType", "Amount"}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: []*testBasicDataTableRow{},
}
columnMapping := map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "TransactionTime",
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "TransactionType",
TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
}
transactionDataTable := CreateNewTransactionDataTableFromBasicDataTable(basicDataTable, columnMapping)
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_AMOUNT))
assert.False(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION))
assert.False(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_CATEGORY))
}
func TestBasicDataTableToTransactionDataTableWrapper_TransactionRowCount(t *testing.T) {
columns := []string{"TransactionTime", "TransactionType", "Amount"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"2024-01-01", "1", "100"},
},
{
rowId: "2",
rowColumns: []string{"2024-01-02", "2", "200"},
},
{
rowId: "3",
rowColumns: []string{"2024-01-03", "1", "300"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
columnMapping := map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "TransactionTime",
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "TransactionType",
TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
}
transactionDataTable := CreateNewTransactionDataTableFromBasicDataTable(basicDataTable, columnMapping)
assert.Equal(t, len(rows), transactionDataTable.TransactionRowCount())
}
func TestBasicDataTableToTransactionDataTableWrapper_TransactionRowIterator(t *testing.T) {
columns := []string{"TransactionTime", "TransactionType", "Amount"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"2024-01-01", "1", "100"},
},
{
rowId: "2",
rowColumns: []string{"2024-01-02", "2", "200"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
columnMapping := map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "TransactionTime",
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "TransactionType",
TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
}
transactionDataTable := CreateNewTransactionDataTableFromBasicDataTable(basicDataTable, columnMapping)
iterator := transactionDataTable.TransactionRowIterator()
assert.True(t, iterator.HasNext())
firstRow, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.NotNil(t, firstRow)
assert.True(t, firstRow.IsValid())
assert.Equal(t, "2024-01-01", firstRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
assert.Equal(t, "1", firstRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
assert.Equal(t, "100", firstRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
assert.True(t, iterator.HasNext())
secondRow, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.NotNil(t, secondRow)
assert.True(t, secondRow.IsValid())
assert.Equal(t, "2024-01-02", secondRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
assert.Equal(t, "2", secondRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
assert.Equal(t, "200", secondRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
assert.False(t, iterator.HasNext())
emptyRow, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.Nil(t, emptyRow)
}
func TestBasicDataTableToTransactionDataTableWrapper_TransactionRowIterator_EmptyRow(t *testing.T) {
columns := []string{"TransactionTime", "TransactionType", "Amount"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{""},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
columnMapping := map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "TransactionTime",
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "TransactionType",
TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
}
transactionDataTable := CreateNewTransactionDataTableFromBasicDataTable(basicDataTable, columnMapping)
iterator := transactionDataTable.TransactionRowIterator()
assert.True(t, iterator.HasNext())
row, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.NotNil(t, row)
assert.False(t, row.IsValid())
}
func TestBasicDataTableToTransactionDataTableWrapper_TransactionRowIterator_InvalidRow(t *testing.T) {
columns := []string{"TransactionTime", "TransactionType", "Amount"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"2024-01-01", "1"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
columnMapping := map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "TransactionTime",
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "TransactionType",
TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
}
transactionDataTable := CreateNewTransactionDataTableFromBasicDataTable(basicDataTable, columnMapping)
iterator := transactionDataTable.TransactionRowIterator()
assert.True(t, iterator.HasNext())
row, err := iterator.Next(nil, nil)
assert.NotNil(t, err)
assert.Equal(t, errs.ErrFewerFieldsInDataRowThanInHeaderRow, err)
assert.Nil(t, row)
}
func TestBasicDataTableToTransactionDataTableWrapper_TransactionRowIterator_WithRowParserAddedColumn(t *testing.T) {
columns := []string{"TransactionTime", "TransactionType", "Amount"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"2024-01-01", "1", "100"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
columnMapping := map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "TransactionTime",
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "TransactionType",
TRANSACTION_DATA_TABLE_AMOUNT: "Amount",
TRANSACTION_DATA_TABLE_DESCRIPTION: "Description",
}
transactionDataTable := CreateNewTransactionDataTableFromBasicDataTableWithRowParser(basicDataTable, columnMapping, &testTransactionDataRowParser{})
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION))
iterator := transactionDataTable.TransactionRowIterator()
assert.True(t, iterator.HasNext())
row, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.NotNil(t, row)
assert.True(t, row.IsValid())
assert.Equal(t, "Test Description", row.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION))
}
@@ -0,0 +1,258 @@
package datatable
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
type testCommonDataTable struct {
headerColumns []string
dataRows []*testCommonDataTableRow
}
type testCommonDataTableRow struct {
rowId string
rowData map[string]string
}
type testCommonDataTableRowIterator struct {
dataTable *testCommonDataTable
currentIndex int
}
func (t *testCommonDataTable) DataRowCount() int {
return len(t.dataRows)
}
func (t *testCommonDataTable) HeaderColumnCount() int {
return len(t.headerColumns)
}
func (t *testCommonDataTable) HasColumn(columnName string) bool {
for _, header := range t.headerColumns {
if header == columnName {
return true
}
}
return false
}
func (t *testCommonDataTable) DataRowIterator() CommonDataTableRowIterator {
return &testCommonDataTableRowIterator{
dataTable: t,
currentIndex: -1,
}
}
func (t *testCommonDataTableRow) GetData(dataKey string) string {
return t.rowData[dataKey]
}
func (t *testCommonDataTableRow) HasData(dataKey string) bool {
_, exists := t.rowData[dataKey]
return exists
}
func (t *testCommonDataTableRow) ColumnCount() int {
return len(t.rowData)
}
func (t *testCommonDataTableRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.dataRows)
}
func (t *testCommonDataTableRowIterator) Next() CommonDataTableRow {
if !t.HasNext() {
return nil
}
t.currentIndex++
return t.dataTable.dataRows[t.currentIndex]
}
func (t *testCommonDataTableRowIterator) CurrentRowId() string {
if t.currentIndex < 0 || t.currentIndex >= len(t.dataTable.dataRows) {
return ""
}
return t.dataTable.dataRows[t.currentIndex].rowId
}
type testCommonTransactionDataRowParser struct {
returnError bool
}
func (p *testCommonTransactionDataRowParser) Parse(ctx core.Context, user *models.User, dataRow CommonDataTableRow, rowId string) (map[TransactionDataTableColumn]string, bool, error) {
if p.returnError {
return nil, false, errs.ErrOperationFailed
}
rowData := make(map[TransactionDataTableColumn]string)
rowData[TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = dataRow.GetData("TransactionTime")
rowData[TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataRow.GetData("TransactionType")
rowData[TRANSACTION_DATA_TABLE_AMOUNT] = dataRow.GetData("Amount")
rowData[TRANSACTION_DATA_TABLE_DESCRIPTION] = "Test Description"
return rowData, true, nil
}
func TestCommonDataTableToTransactionDataTableWrapper_HasColumn(t *testing.T) {
basicDataTable := &testCommonDataTable{
headerColumns: []string{"TransactionTime", "TransactionType", "Amount"},
dataRows: []*testCommonDataTableRow{},
}
supportedColumns := map[TransactionDataTableColumn]bool{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
TRANSACTION_DATA_TABLE_AMOUNT: true,
}
transactionDataTable := CreateNewTransactionDataTableFromCommonDataTable(basicDataTable, supportedColumns, &testCommonTransactionDataRowParser{})
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
assert.True(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_AMOUNT))
assert.False(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_CATEGORY))
assert.False(t, transactionDataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION))
}
func TestCommonDataTableToTransactionDataTableWrapper_TransactionRowCount(t *testing.T) {
rows := []*testCommonDataTableRow{
{
rowId: "1",
rowData: map[string]string{
"TransactionTime": "2024-01-01",
"TransactionType": "1",
"Amount": "100",
},
},
{
rowId: "2",
rowData: map[string]string{
"TransactionTime": "2024-01-02",
"TransactionType": "2",
"Amount": "200",
},
},
{
rowId: "3",
rowData: map[string]string{
"TransactionTime": "2024-01-03",
"TransactionType": "1",
"Amount": "300",
},
},
}
basicDataTable := &testCommonDataTable{
headerColumns: []string{"TransactionTime", "TransactionType", "Amount"},
dataRows: rows,
}
supportedColumns := map[TransactionDataTableColumn]bool{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
TRANSACTION_DATA_TABLE_AMOUNT: true,
}
transactionDataTable := CreateNewTransactionDataTableFromCommonDataTable(basicDataTable, supportedColumns, &testCommonTransactionDataRowParser{})
assert.Equal(t, len(rows), transactionDataTable.TransactionRowCount())
}
func TestCommonDataTableToTransactionDataTableWrapper_TransactionRowIterator(t *testing.T) {
rows := []*testCommonDataTableRow{
{
rowId: "1",
rowData: map[string]string{
"TransactionTime": "2024-01-01",
"TransactionType": "1",
"Amount": "100",
},
},
{
rowId: "2",
rowData: map[string]string{
"TransactionTime": "2024-01-02",
"TransactionType": "2",
"Amount": "200",
},
},
}
basicDataTable := &testCommonDataTable{
headerColumns: []string{"TransactionTime", "TransactionType", "Amount"},
dataRows: rows,
}
supportedColumns := map[TransactionDataTableColumn]bool{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
TRANSACTION_DATA_TABLE_AMOUNT: true,
}
transactionDataTable := CreateNewTransactionDataTableFromCommonDataTable(basicDataTable, supportedColumns, &testCommonTransactionDataRowParser{})
iterator := transactionDataTable.TransactionRowIterator()
assert.True(t, iterator.HasNext())
firstRow, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.NotNil(t, firstRow)
assert.True(t, firstRow.IsValid())
assert.Equal(t, "2024-01-01", firstRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
assert.Equal(t, "1", firstRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
assert.Equal(t, "100", firstRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
assert.Equal(t, "", firstRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION))
assert.True(t, iterator.HasNext())
secondRow, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.NotNil(t, secondRow)
assert.True(t, secondRow.IsValid())
assert.Equal(t, "2024-01-02", secondRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME))
assert.Equal(t, "2", secondRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
assert.Equal(t, "200", secondRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
assert.Equal(t, "", secondRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION))
assert.False(t, iterator.HasNext())
emptyRow, err := iterator.Next(nil, nil)
assert.Nil(t, err)
assert.Nil(t, emptyRow)
}
func TestCommonDataTableToTransactionDataTableWrapper_TransactionRowIterator_EOF(t *testing.T) {
rows := []*testCommonDataTableRow{
{
rowId: "1",
rowData: map[string]string{
"TransactionTime": "2024-01-01",
"TransactionType": "1",
"Amount": "100",
},
},
}
basicDataTable := &testCommonDataTable{
headerColumns: []string{"TransactionTime", "TransactionType", "Amount"},
dataRows: rows,
}
supportedColumns := map[TransactionDataTableColumn]bool{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
TRANSACTION_DATA_TABLE_AMOUNT: true,
}
transactionDataTable := CreateNewTransactionDataTableFromCommonDataTable(basicDataTable, supportedColumns, &testCommonTransactionDataRowParser{returnError: true})
iterator := transactionDataTable.TransactionRowIterator()
assert.True(t, iterator.HasNext())
row, err := iterator.Next(nil, nil)
assert.EqualError(t, err, errs.ErrOperationFailed.Message)
assert.Nil(t, row)
}
@@ -0,0 +1,87 @@
package datatable
// SubBasicDataTable defines the structure of sub basic data table
type SubBasicDataTable struct {
baseTable BasicDataTable
fromIndex int
toIndex int
}
// SubBasicDataTableRowIterator defines the structure of sub basic data table row iterator
type SubBasicDataTableRowIterator struct {
dataTable *SubBasicDataTable
innerIterator BasicDataTableRowIterator
currentIndex int
}
// DataRowCount returns the total count of data row
func (t *SubBasicDataTable) DataRowCount() int {
return t.toIndex - t.fromIndex
}
// HeaderColumnNames returns the header column name list
func (t *SubBasicDataTable) HeaderColumnNames() []string {
return t.baseTable.HeaderColumnNames()
}
// DataRowIterator returns the iterator of data row
func (t *SubBasicDataTable) DataRowIterator() BasicDataTableRowIterator {
innerIterator := t.baseTable.DataRowIterator()
currentIndex := -1
// skip rows until reaching the fromIndex
for currentIndex = -1; currentIndex < t.fromIndex-1 && innerIterator.HasNext(); currentIndex++ {
innerIterator.Next()
}
return &SubBasicDataTableRowIterator{
dataTable: t,
innerIterator: innerIterator,
currentIndex: currentIndex,
}
}
// HasNext returns whether the iterator does not reach the end
func (t *SubBasicDataTableRowIterator) HasNext() bool {
return t.currentIndex+1 < t.dataTable.toIndex && t.innerIterator.HasNext()
}
// CurrentRowId returns current row id
func (t *SubBasicDataTableRowIterator) CurrentRowId() string {
return t.innerIterator.CurrentRowId()
}
// Next returns the next basic data row
func (t *SubBasicDataTableRowIterator) Next() BasicDataTableRow {
if t.currentIndex+1 >= t.dataTable.toIndex {
return nil
}
t.currentIndex++
return t.innerIterator.Next()
}
// CreateSubBasicTable returns a sub basic data table that references a portion of the original table
func CreateSubBasicTable(dataTable BasicDataTable, fromIndex, toIndex int) *SubBasicDataTable {
if fromIndex < 0 {
fromIndex = 0
}
if fromIndex > dataTable.DataRowCount() {
fromIndex = dataTable.DataRowCount()
}
if toIndex > dataTable.DataRowCount() {
toIndex = dataTable.DataRowCount()
}
if toIndex < fromIndex {
toIndex = fromIndex
}
return &SubBasicDataTable{
baseTable: dataTable,
fromIndex: fromIndex,
toIndex: toIndex,
}
}
@@ -0,0 +1,140 @@
package datatable
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateSubBasicTable_WithValidInput(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"A1", "B1", "C1"},
},
{
rowId: "2",
rowColumns: []string{"A2", "B2", "C2"},
},
{
rowId: "3",
rowColumns: []string{"A3", "B3", "C3"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
subTable := CreateSubBasicTable(basicDataTable, 1, 2)
assert.Equal(t, 1, subTable.DataRowCount())
assert.Equal(t, columns, subTable.HeaderColumnNames())
}
func TestCreateSubBasicTable_WithInvalidInput(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"A1", "B1", "C1"},
},
{
rowId: "2",
rowColumns: []string{"A2", "B2", "C2"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
subTable := CreateSubBasicTable(basicDataTable, -1, 2)
assert.Equal(t, 0, subTable.fromIndex)
assert.Equal(t, 2, subTable.toIndex)
subTable = CreateSubBasicTable(basicDataTable, 5, 2)
assert.Equal(t, 2, subTable.fromIndex)
assert.Equal(t, 2, subTable.toIndex)
subTable = CreateSubBasicTable(basicDataTable, 0, 5)
assert.Equal(t, 0, subTable.fromIndex)
assert.Equal(t, 2, subTable.toIndex)
subTable = CreateSubBasicTable(basicDataTable, 2, 1)
assert.Equal(t, 2, subTable.fromIndex)
assert.Equal(t, 2, subTable.toIndex)
}
func TestSubBasicDataTable_DataRowIterator(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"A1", "B1", "C1"},
},
{
rowId: "2",
rowColumns: []string{"A2", "B2", "C2"},
},
{
rowId: "3",
rowColumns: []string{"A3", "B3", "C3"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
subTable := CreateSubBasicTable(basicDataTable, 1, 3)
iterator := subTable.DataRowIterator()
assert.True(t, iterator.HasNext())
firstRow := iterator.Next()
assert.NotNil(t, firstRow)
assert.Equal(t, "2", iterator.CurrentRowId())
assert.Equal(t, "A2", firstRow.GetData(0))
assert.Equal(t, "B2", firstRow.GetData(1))
assert.Equal(t, "C2", firstRow.GetData(2))
assert.True(t, iterator.HasNext())
secondRow := iterator.Next()
assert.NotNil(t, secondRow)
assert.Equal(t, "3", iterator.CurrentRowId())
assert.Equal(t, "A3", secondRow.GetData(0))
assert.Equal(t, "B3", secondRow.GetData(1))
assert.Equal(t, "C3", secondRow.GetData(2))
assert.False(t, iterator.HasNext())
assert.Nil(t, iterator.Next())
}
func TestSubBasicDataTable_EmptyDataRange(t *testing.T) {
columns := []string{"Col1", "Col2", "Col3"}
rows := []*testBasicDataTableRow{
{
rowId: "1",
rowColumns: []string{"A1", "B1", "C1"},
},
{
rowId: "2",
rowColumns: []string{"A2", "B2", "C2"},
},
}
basicDataTable := &testBasicDataTable{
headerColumns: columns,
rows: rows,
}
subTable := CreateSubBasicTable(basicDataTable, 1, 1)
assert.Equal(t, 0, subTable.DataRowCount())
iterator := subTable.DataRowIterator()
assert.False(t, iterator.HasNext())
assert.Nil(t, iterator.Next())
}
@@ -153,11 +153,7 @@ func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Contex
return nil, nil, nil, nil, nil, nil, err
}
if !c.hasHeaderLine {
allLines = append([][]string{{}}, allLines...)
}
dataTable := csvconverter.CreateNewCustomCsvBasicDataTable(allLines)
dataTable := csvconverter.CreateNewCustomCsvBasicDataTable(allLines, c.hasHeaderLine)
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat, c.amountDecimalSeparator, c.amountDigitGroupingSymbol)
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(customTransactionTypeNameMapping, c.geoLocationSeparator, c.geoLocationOrder, c.transactionTagSeparator)
@@ -14,6 +14,7 @@ import (
type ExcelMSCFBFileBasicDataTable struct {
workbook *xls.WorkBook
headerLineColumnNames []string
hasTitleLine bool
}
// ExcelMSCFBFileBasicDataTableRow defines the structure of excel (microsoft compound file binary) file data table row
@@ -26,7 +27,7 @@ type ExcelMSCFBFileBasicDataTableRow struct {
type ExcelMSCFBFileBasicDataTableRowIterator struct {
dataTable *ExcelMSCFBFileBasicDataTable
currentSheetIndex int
currentRowIndexInSheet uint16
currentRowIndexInSheet int
}
// DataRowCount returns the total count of data row
@@ -36,11 +37,23 @@ func (t *ExcelMSCFBFileBasicDataTable) DataRowCount() int {
for i := 0; i < t.workbook.NumSheets(); i++ {
sheet := t.workbook.GetSheet(i)
if sheet.MaxRow < 1 {
if sheet == nil {
continue
}
totalDataRowCount += int(sheet.MaxRow)
if t.hasTitleLine {
if sheet.MaxRow < 1 {
continue
}
totalDataRowCount += int(sheet.MaxRow)
} else {
if sheet.MaxRow <= 0 && sheet.Row(0) == nil {
continue
}
totalDataRowCount += int(sheet.MaxRow) + 1
}
}
return totalDataRowCount
@@ -48,15 +61,25 @@ func (t *ExcelMSCFBFileBasicDataTable) DataRowCount() int {
// HeaderColumnNames returns the header column name list
func (t *ExcelMSCFBFileBasicDataTable) HeaderColumnNames() []string {
if !t.hasTitleLine {
return nil
}
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *ExcelMSCFBFileBasicDataTable) DataRowIterator() datatable.BasicDataTableRowIterator {
startIndex := -1
if t.hasTitleLine {
startIndex = 0
}
return &ExcelMSCFBFileBasicDataTableRowIterator{
dataTable: t,
currentSheetIndex: 0,
currentRowIndexInSheet: 0,
currentRowIndexInSheet: startIndex,
}
}
@@ -82,15 +105,21 @@ func (t *ExcelMSCFBFileBasicDataTableRowIterator) HasNext() bool {
currentSheet := workbook.GetSheet(t.currentSheetIndex)
if t.currentRowIndexInSheet+1 <= currentSheet.MaxRow {
if t.currentRowIndexInSheet+1 <= int(currentSheet.MaxRow) && currentSheet.Row(t.currentRowIndexInSheet+1) != nil {
return true
}
for i := t.currentSheetIndex + 1; i < workbook.NumSheets(); i++ {
sheet := workbook.GetSheet(i)
if sheet.MaxRow < 1 {
continue
if t.dataTable.hasTitleLine {
if sheet.MaxRow < 1 {
continue
}
} else {
if sheet.MaxRow <= 0 && sheet.Row(0) == nil {
continue
}
}
return true
@@ -107,20 +136,22 @@ func (t *ExcelMSCFBFileBasicDataTableRowIterator) CurrentRowId() string {
// Next returns the next basic data row
func (t *ExcelMSCFBFileBasicDataTableRowIterator) Next() datatable.BasicDataTableRow {
workbook := t.dataTable.workbook
currentRowIndexInTable := t.currentRowIndexInSheet
for i := t.currentSheetIndex; i < workbook.NumSheets(); i++ {
sheet := workbook.GetSheet(i)
if currentRowIndexInTable+1 <= sheet.MaxRow {
if t.currentRowIndexInSheet+1 <= int(sheet.MaxRow) && sheet.Row(t.currentRowIndexInSheet+1) != nil {
t.currentRowIndexInSheet++
currentRowIndexInTable = t.currentRowIndexInSheet
break
}
t.currentSheetIndex++
t.currentRowIndexInSheet = 0
currentRowIndexInTable = 0
if t.dataTable.hasTitleLine {
t.currentRowIndexInSheet = 0
} else {
t.currentRowIndexInSheet = -1
}
}
if t.currentSheetIndex >= workbook.NumSheets() {
@@ -129,7 +160,7 @@ func (t *ExcelMSCFBFileBasicDataTableRowIterator) Next() datatable.BasicDataTabl
currentSheet := workbook.GetSheet(t.currentSheetIndex)
if t.currentRowIndexInSheet > currentSheet.MaxRow {
if t.currentRowIndexInSheet > int(currentSheet.MaxRow) || currentSheet.Row(t.currentRowIndexInSheet) == nil {
return nil
}
@@ -140,7 +171,7 @@ func (t *ExcelMSCFBFileBasicDataTableRowIterator) Next() datatable.BasicDataTabl
}
// CreateNewExcelMSCFBFileBasicDataTable returns excel (microsoft compound file binary) data table by file binary data
func CreateNewExcelMSCFBFileBasicDataTable(data []byte) (datatable.BasicDataTable, error) {
func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (datatable.BasicDataTable, error) {
reader := bytes.NewReader(data)
workbook, err := xls.OpenReader(reader, "")
@@ -148,12 +179,12 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte) (datatable.BasicDataTabl
return nil, err
}
var headerRowItems []string
var firstRowItems []string
for i := 0; i < workbook.NumSheets(); i++ {
sheet := workbook.GetSheet(i)
if sheet.MaxRow < 0 {
if sheet.MaxRow <= 0 && sheet.Row(0) == nil {
continue
}
@@ -171,21 +202,28 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte) (datatable.BasicDataTabl
break
}
headerRowItems = append(headerRowItems, headerItem)
firstRowItems = append(firstRowItems, headerItem)
}
} else {
for j := 0; j <= min(row.LastCol(), len(headerRowItems)-1); j++ {
for j := 0; j <= min(row.LastCol(), len(firstRowItems)-1); j++ {
headerItem := row.Col(j)
if headerItem != headerRowItems[j] {
if headerItem != firstRowItems[j] {
return nil, errs.ErrFieldsInMultiTableAreDifferent
}
}
}
}
var headerLineColumnNames []string = nil
if hasTitleLine {
headerLineColumnNames = firstRowItems
}
return &ExcelMSCFBFileBasicDataTable{
workbook: workbook,
headerLineColumnNames: headerRowItems,
headerLineColumnNames: headerLineColumnNames,
hasTitleLine: hasTitleLine,
}, nil
}
@@ -13,7 +13,16 @@ func TestExcelMSCFBFileBasicDataTableDataRowCount(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Equal(t, 3, datatable.DataRowCount())
}
func TestExcelMSCFBFileBasicDataTableDataRowCount_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 2, datatable.DataRowCount())
}
@@ -22,7 +31,16 @@ func TestExcelMSCFBFileBasicDataTableDataRowCount_MultipleSheets(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Equal(t, 9, datatable.DataRowCount())
}
func TestExcelMSCFBFileBasicDataTableDataRowCount_MultipleSheets_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 5, datatable.DataRowCount())
}
@@ -31,7 +49,7 @@ func TestExcelMSCFBFileBasicDataTableDataRowCount_OnlyHeaderLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 0, datatable.DataRowCount())
}
@@ -40,7 +58,11 @@ func TestExcelMSCFBFileBasicDataTableDataRowCount_EmptyContent(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Equal(t, 0, datatable.DataRowCount())
datatable, err = CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 0, datatable.DataRowCount())
}
@@ -49,7 +71,17 @@ func TestExcelMSCFBFileBasicDataTableHeaderColumnNames(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Nil(t, datatable.HeaderColumnNames())
}
func TestExcelMSCFBFileBasicDataTableHeaderColumnNames_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames())
}
@@ -57,15 +89,47 @@ func TestExcelMSCFBFileBasicDataTableHeaderColumnNames_EmptyContent(t *testing.T
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Nil(t, datatable.HeaderColumnNames())
datatable, err = CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Nil(t, datatable.HeaderColumnNames())
}
func TestExcelMSCFBFileBasicDataTableRowIterator(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowIterator(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
// data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// data row 3
assert.NotNil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row 4
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestExcelMSCFBFileBasicDataRowIterator_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
@@ -86,11 +150,66 @@ func TestExcelMSCFBFileBasicDataTableRowIterator(t *testing.T) {
assert.False(t, iterator.HasNext())
}
func TestExcelMSCFBFileBasicDataTableRowIterator_MultipleSheets(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowIterator_MultipleSheets(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
// sheet 1 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 1 data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 1 data row 3
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 3 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 3 data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 4 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 5 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 5 data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 5 data row 3
assert.NotNil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestExcelMSCFBFileBasicDataRowIterator_MultipleSheets_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
@@ -123,11 +242,12 @@ func TestExcelMSCFBFileBasicDataTableRowIterator_MultipleSheets(t *testing.T) {
assert.False(t, iterator.HasNext())
}
func TestExcelMSCFBFileBasicDataTableRowIterator_OnlyHeaderLine(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowIterator_OnlyHeaderLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.False(t, iterator.HasNext())
@@ -140,11 +260,12 @@ func TestExcelMSCFBFileBasicDataTableRowIterator_OnlyHeaderLine(t *testing.T) {
assert.False(t, iterator.HasNext())
}
func TestExcelMSCFBFileBasicDataTableRowIterator_EmptyContent(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowIterator_EmptyContent(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.False(t, iterator.HasNext())
@@ -155,13 +276,27 @@ func TestExcelMSCFBFileBasicDataTableRowIterator_EmptyContent(t *testing.T) {
// not existed data row 2
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
datatable, err = CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator = datatable.DataRowIterator()
assert.False(t, iterator.HasNext())
// not existed data row 1
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row 2
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestExcelMSCFBFileBasicDataTableRowColumnCount(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowColumnCount(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
@@ -171,11 +306,36 @@ func TestExcelMSCFBFileBasicDataTableRowColumnCount(t *testing.T) {
assert.EqualValues(t, 4, row2.ColumnCount())
}
func TestExcelMSCFBFileBasicDataTableRowGetData(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowGetData(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
assert.Equal(t, "A1", row1.GetData(0))
assert.Equal(t, "B1", row1.GetData(1))
assert.Equal(t, "C1", row1.GetData(2))
row2 := iterator.Next()
assert.Equal(t, "A2", row2.GetData(0))
assert.Equal(t, "B2", row2.GetData(1))
assert.Equal(t, "C2", row2.GetData(2))
row3 := iterator.Next()
assert.Equal(t, "A3", row3.GetData(0))
assert.Equal(t, "B3", row3.GetData(1))
assert.Equal(t, "C3", row3.GetData(2))
}
func TestExcelMSCFBFileBasicDataRowGetData_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
@@ -189,22 +349,80 @@ func TestExcelMSCFBFileBasicDataTableRowGetData(t *testing.T) {
assert.Equal(t, "C3", row2.GetData(2))
}
func TestExcelMSCFBFileBasicDataTableRowGetData_GetNotExistedColumnData(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowGetData_GetNotExistedColumnData(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
assert.Equal(t, "", row1.GetData(3))
}
func TestExcelMSCFBFileBasicDataTableRowGetData_MultipleSheets(t *testing.T) {
func TestExcelMSCFBFileBasicDataRowGetData_MultipleSheets(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
sheet1Row1 := iterator.Next()
assert.Equal(t, "A1", sheet1Row1.GetData(0))
assert.Equal(t, "B1", sheet1Row1.GetData(1))
assert.Equal(t, "C1", sheet1Row1.GetData(2))
sheet1Row2 := iterator.Next()
assert.Equal(t, "1-A2", sheet1Row2.GetData(0))
assert.Equal(t, "1-B2", sheet1Row2.GetData(1))
assert.Equal(t, "1-C2", sheet1Row2.GetData(2))
sheet1Row3 := iterator.Next()
assert.Equal(t, "1-A3", sheet1Row3.GetData(0))
assert.Equal(t, "1-B3", sheet1Row3.GetData(1))
assert.Equal(t, "1-C3", sheet1Row3.GetData(2))
// skip empty sheet2
sheet3Row1 := iterator.Next()
assert.Equal(t, "A1", sheet3Row1.GetData(0))
assert.Equal(t, "B1", sheet3Row1.GetData(1))
assert.Equal(t, "C1", sheet3Row1.GetData(2))
sheet3Row2 := iterator.Next()
assert.Equal(t, "3-A2", sheet3Row2.GetData(0))
assert.Equal(t, "3-B2", sheet3Row2.GetData(1))
assert.Equal(t, "", sheet3Row2.GetData(2))
sheet4Row1 := iterator.Next()
assert.Equal(t, "A1", sheet4Row1.GetData(0))
assert.Equal(t, "B1", sheet4Row1.GetData(1))
assert.Equal(t, "C1", sheet4Row1.GetData(2))
sheet5Row1 := iterator.Next()
assert.Equal(t, "A1", sheet5Row1.GetData(0))
assert.Equal(t, "B1", sheet5Row1.GetData(1))
assert.Equal(t, "C1", sheet5Row1.GetData(2))
sheet5Row2 := iterator.Next()
assert.Equal(t, "5-A2", sheet5Row2.GetData(0))
assert.Equal(t, "5-B2", sheet5Row2.GetData(1))
assert.Equal(t, "5-C2", sheet5Row2.GetData(2))
sheet5Row3 := iterator.Next()
assert.Equal(t, "5-A3", sheet5Row3.GetData(0))
assert.Equal(t, "5-B3", sheet5Row3.GetData(1))
assert.Equal(t, "5-C3", sheet5Row3.GetData(2))
}
func TestExcelMSCFBFileBasicDataRowGetData_MultipleSheets_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
datatable, err := CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
sheet1Row1 := iterator.Next()
@@ -241,6 +459,6 @@ func TestCreateNewExcelMSCFBFileBasicDataTable_MultipleSheetsWithDifferentHeader
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xls")
assert.Nil(t, err)
_, err = CreateNewExcelMSCFBFileBasicDataTable(testdata)
_, err = CreateNewExcelMSCFBFileBasicDataTable(testdata, true)
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
}
@@ -20,6 +20,7 @@ type excelOOXMLSheet struct {
type ExcelOOXMLFileBasicDataTable struct {
sheets []*excelOOXMLSheet
headerLineColumnNames []string
hasTitleLine bool
}
// ExcelOOXMLFileBasicDataTableRow defines the structure of excel (Office Open XML) file data table row
@@ -47,7 +48,11 @@ func (t *ExcelOOXMLFileBasicDataTable) DataRowCount() int {
continue
}
totalDataRowCount += len(sheet.allData) - 1
if t.hasTitleLine {
totalDataRowCount += len(sheet.allData) - 1
} else {
totalDataRowCount += len(sheet.allData)
}
}
return totalDataRowCount
@@ -55,15 +60,25 @@ func (t *ExcelOOXMLFileBasicDataTable) DataRowCount() int {
// HeaderColumnNames returns the header column name list
func (t *ExcelOOXMLFileBasicDataTable) HeaderColumnNames() []string {
if !t.hasTitleLine {
return nil
}
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *ExcelOOXMLFileBasicDataTable) DataRowIterator() datatable.BasicDataTableRowIterator {
startIndex := -1
if t.hasTitleLine {
startIndex = 0
}
return &ExcelOOXMLFileBasicDataTableRowIterator{
dataTable: t,
currentSheetIndex: 0,
currentRowIndexInSheet: 0,
currentRowIndexInSheet: startIndex,
}
}
@@ -98,8 +113,14 @@ func (t *ExcelOOXMLFileBasicDataTableRowIterator) HasNext() bool {
for i := t.currentSheetIndex + 1; i < len(sheets); i++ {
sheet := sheets[i]
if len(sheet.allData) <= 1 {
continue
if t.dataTable.hasTitleLine {
if len(sheet.allData) <= 1 {
continue
}
} else {
if len(sheet.allData) <= 0 {
continue
}
}
return true
@@ -116,20 +137,22 @@ func (t *ExcelOOXMLFileBasicDataTableRowIterator) CurrentRowId() string {
// Next returns the next basic data row
func (t *ExcelOOXMLFileBasicDataTableRowIterator) Next() datatable.BasicDataTableRow {
sheets := t.dataTable.sheets
currentRowIndexInTable := t.currentRowIndexInSheet
for i := t.currentSheetIndex; i < len(sheets); i++ {
sheet := sheets[i]
if currentRowIndexInTable+1 < len(sheet.allData) {
if t.currentRowIndexInSheet+1 < len(sheet.allData) {
t.currentRowIndexInSheet++
currentRowIndexInTable = t.currentRowIndexInSheet
break
}
t.currentSheetIndex++
t.currentRowIndexInSheet = 0
currentRowIndexInTable = 0
if t.dataTable.hasTitleLine {
t.currentRowIndexInSheet = 0
} else {
t.currentRowIndexInSheet = -1
}
}
if t.currentSheetIndex >= len(sheets) {
@@ -150,7 +173,7 @@ func (t *ExcelOOXMLFileBasicDataTableRowIterator) Next() datatable.BasicDataTabl
}
// CreateNewExcelOOXMLFileBasicDataTable returns excel (Office Open XML) data table by file binary data
func CreateNewExcelOOXMLFileBasicDataTable(data []byte) (datatable.BasicDataTable, error) {
func CreateNewExcelOOXMLFileBasicDataTable(data []byte, hasTitleLine bool) (datatable.BasicDataTable, error) {
reader := bytes.NewReader(data)
file, err := excelize.OpenReader(reader)
@@ -161,7 +184,7 @@ func CreateNewExcelOOXMLFileBasicDataTable(data []byte) (datatable.BasicDataTabl
}
sheetNames := file.GetSheetList()
var headerRowItems []string
var firstRowItems []string
var sheets []*excelOOXMLSheet
for i := 0; i < len(sheetNames); i++ {
@@ -186,13 +209,13 @@ func CreateNewExcelOOXMLFileBasicDataTable(data []byte) (datatable.BasicDataTabl
break
}
headerRowItems = append(headerRowItems, headerItem)
firstRowItems = append(firstRowItems, headerItem)
}
} else {
for j := 0; j < min(len(row), len(headerRowItems)); j++ {
for j := 0; j < min(len(row), len(firstRowItems)); j++ {
headerItem := row[j]
if headerItem != headerRowItems[j] {
if headerItem != firstRowItems[j] {
return nil, errs.ErrFieldsInMultiTableAreDifferent
}
}
@@ -204,8 +227,15 @@ func CreateNewExcelOOXMLFileBasicDataTable(data []byte) (datatable.BasicDataTabl
})
}
var headerLineColumnNames []string = nil
if hasTitleLine {
headerLineColumnNames = firstRowItems
}
return &ExcelOOXMLFileBasicDataTable{
sheets: sheets,
headerLineColumnNames: headerRowItems,
headerLineColumnNames: headerLineColumnNames,
hasTitleLine: hasTitleLine,
}, nil
}
@@ -13,7 +13,16 @@ func TestExcelOOXMLFileBasicDataTableDataRowCount(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Equal(t, 3, datatable.DataRowCount())
}
func TestExcelOOXMLFileBasicDataTableDataRowCount_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 2, datatable.DataRowCount())
}
@@ -22,7 +31,16 @@ func TestExcelOOXMLFileBasicDataTableDataRowCount_MultipleSheets(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Equal(t, 9, datatable.DataRowCount())
}
func TestExcelOOXMLFileBasicDataTableDataRowCount_MultipleSheets_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 5, datatable.DataRowCount())
}
@@ -31,7 +49,7 @@ func TestExcelOOXMLFileBasicDataTableDataRowCount_OnlyHeaderLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 0, datatable.DataRowCount())
}
@@ -40,7 +58,11 @@ func TestExcelOOXMLFileBasicDataTableDataRowCount_EmptyContent(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Equal(t, 0, datatable.DataRowCount())
datatable, err = CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Equal(t, 0, datatable.DataRowCount())
}
@@ -49,7 +71,17 @@ func TestExcelOOXMLFileBasicDataTableHeaderColumnNames(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Nil(t, datatable.HeaderColumnNames())
}
func TestExcelOOXMLFileBasicDataTableHeaderColumnNames_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames())
}
@@ -57,7 +89,12 @@ func TestExcelOOXMLFileBasicDataTableHeaderColumnNames_EmptyContent(t *testing.T
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
assert.Nil(t, datatable.HeaderColumnNames())
datatable, err = CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
assert.Nil(t, datatable.HeaderColumnNames())
}
@@ -65,7 +102,34 @@ func TestExcelOOXMLFileBasicDataRowIterator(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
// data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// data row 3
assert.NotNil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row 4
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestExcelOOXMLFileBasicDataRowIterator_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
@@ -90,7 +154,62 @@ func TestExcelOOXMLFileBasicDataRowIterator_MultipleSheets(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
// sheet 1 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 1 data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 1 data row 3
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 3 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 3 data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 4 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 5 data row 1
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 5 data row 2
assert.NotNil(t, iterator.Next())
assert.True(t, iterator.HasNext())
// sheet 5 data row 3
assert.NotNil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestExcelOOXMLFileBasicDataRowIterator_MultipleSheets_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.True(t, iterator.HasNext())
@@ -127,7 +246,8 @@ func TestExcelOOXMLFileBasicDataRowIterator_OnlyHeaderLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.False(t, iterator.HasNext())
@@ -144,7 +264,8 @@ func TestExcelOOXMLFileBasicDataRowIterator_EmptyContent(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
assert.False(t, iterator.HasNext())
@@ -155,13 +276,27 @@ func TestExcelOOXMLFileBasicDataRowIterator_EmptyContent(t *testing.T) {
// not existed data row 2
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
datatable, err = CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator = datatable.DataRowIterator()
assert.False(t, iterator.HasNext())
// not existed data row 1
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
// not existed data row 2
assert.Nil(t, iterator.Next())
assert.False(t, iterator.HasNext())
}
func TestExcelOOXMLFileBasicDataRowColumnCount(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
@@ -175,7 +310,32 @@ func TestExcelOOXMLFileBasicDataRowGetData(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
assert.Equal(t, "A1", row1.GetData(0))
assert.Equal(t, "B1", row1.GetData(1))
assert.Equal(t, "C1", row1.GetData(2))
row2 := iterator.Next()
assert.Equal(t, "A2", row2.GetData(0))
assert.Equal(t, "B2", row2.GetData(1))
assert.Equal(t, "C2", row2.GetData(2))
row3 := iterator.Next()
assert.Equal(t, "A3", row3.GetData(0))
assert.Equal(t, "B3", row3.GetData(1))
assert.Equal(t, "C3", row3.GetData(2))
}
func TestExcelOOXMLFileBasicDataRowGetData_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
@@ -193,7 +353,8 @@ func TestExcelOOXMLFileBasicDataRowGetData_GetNotExistedColumnData(t *testing.T)
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
row1 := iterator.Next()
@@ -204,7 +365,64 @@ func TestExcelOOXMLFileBasicDataRowGetData_MultipleSheets(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, false)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
sheet1Row1 := iterator.Next()
assert.Equal(t, "A1", sheet1Row1.GetData(0))
assert.Equal(t, "B1", sheet1Row1.GetData(1))
assert.Equal(t, "C1", sheet1Row1.GetData(2))
sheet1Row2 := iterator.Next()
assert.Equal(t, "1-A2", sheet1Row2.GetData(0))
assert.Equal(t, "1-B2", sheet1Row2.GetData(1))
assert.Equal(t, "1-C2", sheet1Row2.GetData(2))
sheet1Row3 := iterator.Next()
assert.Equal(t, "1-A3", sheet1Row3.GetData(0))
assert.Equal(t, "1-B3", sheet1Row3.GetData(1))
assert.Equal(t, "1-C3", sheet1Row3.GetData(2))
// skip empty sheet2
sheet3Row1 := iterator.Next()
assert.Equal(t, "A1", sheet3Row1.GetData(0))
assert.Equal(t, "B1", sheet3Row1.GetData(1))
assert.Equal(t, "C1", sheet3Row1.GetData(2))
sheet3Row2 := iterator.Next()
assert.Equal(t, "3-A2", sheet3Row2.GetData(0))
assert.Equal(t, "3-B2", sheet3Row2.GetData(1))
assert.Equal(t, "", sheet3Row2.GetData(2))
sheet4Row1 := iterator.Next()
assert.Equal(t, "A1", sheet4Row1.GetData(0))
assert.Equal(t, "B1", sheet4Row1.GetData(1))
assert.Equal(t, "C1", sheet4Row1.GetData(2))
sheet5Row1 := iterator.Next()
assert.Equal(t, "A1", sheet5Row1.GetData(0))
assert.Equal(t, "B1", sheet5Row1.GetData(1))
assert.Equal(t, "C1", sheet5Row1.GetData(2))
sheet5Row2 := iterator.Next()
assert.Equal(t, "5-A2", sheet5Row2.GetData(0))
assert.Equal(t, "5-B2", sheet5Row2.GetData(1))
assert.Equal(t, "5-C2", sheet5Row2.GetData(2))
sheet5Row3 := iterator.Next()
assert.Equal(t, "5-A3", sheet5Row3.GetData(0))
assert.Equal(t, "5-B3", sheet5Row3.GetData(1))
assert.Equal(t, "5-C3", sheet5Row3.GetData(2))
}
func TestExcelOOXMLFileBasicDataRowGetData_MultipleSheets_HasTitleLine(t *testing.T) {
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
datatable, err := CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.Nil(t, err)
iterator := datatable.DataRowIterator()
sheet1Row1 := iterator.Next()
@@ -241,6 +459,6 @@ func TestCreateNewExcelOOXMLFileBasicDataTable_MultipleSheetsWithDifferentHeader
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xlsx")
assert.Nil(t, err)
_, err = CreateNewExcelOOXMLFileBasicDataTable(testdata)
_, err = CreateNewExcelOOXMLFileBasicDataTable(testdata, true)
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
}
@@ -2,14 +2,13 @@ package feidee
import (
"bytes"
"encoding/csv"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"io"
"strings"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
csvdatatable "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -60,7 +59,13 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx c
fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback))
dataTable, err := c.createNewFeideeMymoneyAppBasicDataTable(ctx, reader)
csvDataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, false)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTable, err := createNewFeideeMymoneyAppTransactionBasicDataTable(ctx, csvDataTable)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
@@ -89,54 +94,6 @@ func (c *feideeMymoneyAppTransactionDataCsvFileImporter) ParseImportedData(ctx c
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyAppBasicDataTable(ctx core.Context, reader io.Reader) (datatable.BasicDataTable, error) {
csvReader := csv.NewReader(reader)
csvReader.FieldsPerRecord = -1
allOriginalLines := make([][]string, 0)
hasFileHeader := false
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] cannot parse feidee mymoney csv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if !hasFileHeader {
if len(items) <= 0 {
continue
} else if strings.Index(items[0], feideeMymoneyAppTransactionDataCsvFileHeader) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ","))
continue
}
}
allOriginalLines = append(allOriginalLines, items)
}
if !hasFileHeader {
return nil, errs.ErrInvalidFileHeader
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_csv_file_importer.createNewFeideeMymoneyAppTransactionDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
dataTable := csvdatatable.CreateNewCustomCsvBasicDataTable(allOriginalLines)
return dataTable, nil
}
func (c *feideeMymoneyAppTransactionDataCsvFileImporter) createNewFeideeMymoneyAppTransactionDataTable(ctx core.Context, commonDataTable datatable.CommonDataTable) (datatable.TransactionDataTable, error) {
newColumns := make([]datatable.TransactionDataTableColumn, 0, 11)
newColumns = append(newColumns, datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
@@ -0,0 +1,52 @@
package feidee
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
)
func createNewFeideeMymoneyAppTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable) (datatable.BasicDataTable, error) {
iterator := originalDataTable.DataRowIterator()
allOriginalLines := make([][]string, 0)
hasFileHeader := false
for iterator.HasNext() {
row := iterator.Next()
if !hasFileHeader {
if row.ColumnCount() <= 0 {
continue
} else if strings.Index(row.GetData(0), feideeMymoneyAppTransactionDataCsvFileHeader) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[feidee_mymoney_app_transaction_data_extrator.createNewFeideeMymoneyAppTransactionBasicDataTable] read unexpected line in row \"%s\" before read file header", iterator.CurrentRowId())
continue
}
}
items := make([]string, row.ColumnCount())
for i := 0; i < row.ColumnCount(); i++ {
items[i] = strings.Trim(row.GetData(i), " ")
}
allOriginalLines = append(allOriginalLines, items)
}
if !hasFileHeader {
return nil, errs.ErrInvalidFileHeader
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[feidee_mymoney_app_transaction_data_extrator.createNewFeideeMymoneyAppTransactionBasicDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
return csv.CreateNewCustomCsvBasicDataTable(allOriginalLines, true), nil
}
@@ -32,7 +32,7 @@ var (
// ParseImportedData returns the imported data by parsing the feidee mymoney (elecloud) transaction xlsx data
func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
dataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data)
dataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data, true)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
@@ -31,7 +31,7 @@ var (
// ParseImportedData returns the imported data by parsing the feidee mymoney (web) transaction xls data
func (c *feideeMymoneyWebTransactionDataXlsFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
dataTable, err := excel.CreateNewExcelMSCFBFileBasicDataTable(data)
dataTable, err := excel.CreateNewExcelMSCFBFileBasicDataTable(data, true)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
@@ -7,24 +7,21 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
var fireflyIIITransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE: true,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: true,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_TAGS: true,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
var fireflyIIITransactionDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "date",
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "type",
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "category",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "source_name",
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "currency_code",
datatable.TRANSACTION_DATA_TABLE_AMOUNT: "amount",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "destination_name",
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "foreign_currency_code",
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: "foreign_amount",
datatable.TRANSACTION_DATA_TABLE_TAGS: "tags",
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "description",
}
var fireflyIIITransactionTypeNameMapping = map[models.TransactionType]string{
@@ -45,27 +42,14 @@ var (
// ParseImportedData returns the imported data by parsing the firefly III transaction csv data
func (c *fireflyIIITransactionDataCsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
reader := bytes.NewReader(data)
dataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader)
dataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, true)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
commonDataTable := datatable.CreateNewCommonDataTableFromBasicDataTable(dataTable)
if !commonDataTable.HasColumn(fireflyIIITransactionTimeColumnName) ||
!commonDataTable.HasColumn(fireflyIIITransactionTypeColumnName) ||
!commonDataTable.HasColumn(fireflyIIITransactionSourceAccountNameColumnName) ||
!commonDataTable.HasColumn(fireflyIIITransactionSourceAccountTypeColumnName) ||
!commonDataTable.HasColumn(fireflyIIITransactionDestinationAccountNameColumnName) ||
!commonDataTable.HasColumn(fireflyIIITransactionDestinationAccountTypeColumnName) ||
!commonDataTable.HasColumn(fireflyIIITransactionAmountColumnName) {
log.Errorf(ctx, "[fireflyiii_transaction_data_csv_file_importer.ParseImportedData] cannot parse Firefly III csv data, because missing essential columns in header row")
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
}
transactionRowParser := createFireflyIIITransactionDataRowParser()
transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, fireflyIIITransactionSupportedColumns, transactionRowParser)
transactionDataTable := datatable.CreateNewTransactionDataTableFromBasicDataTableWithRowParser(dataTable, fireflyIIITransactionDataColumnNameMapping, transactionRowParser)
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(fireflyIIITransactionTypeNameMapping, "", "", ",")
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
@@ -20,11 +20,11 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MinimumValidData(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"+
"Deposit,0.12,2024-09-01T01:23:45+08:00,\"A revenue account\",\"Revenue account\",\"Test Account\",\"Asset account\",\"Test Category\"\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category2\"\n"+
"Transfer,0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
"Deposit,0.12,2024-09-01T01:23:45+08:00,\"A revenue account\",\"Test Account\",\"Test Category\"\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category2\"\n"+
"Transfer,0.05,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -91,16 +91,16 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTime(t *testing
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01 12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTransactionType(t *testing.T) {
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
@@ -109,107 +109,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTransactionType(t
DefaultCurrency: "CNY",
}
// income transactions
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Revenue account\",\"Test Account\",\"Asset account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Revenue account\",\"Test Account\",\"Debt\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
// expense transactions
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Debt\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
// opening balance transactions
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",10.00,2024-09-01T12:34:56+08:00,\"Initial balance\",\"Initial balance account\",\"Test Account\",\"Asset account\",\"\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
// transfer transactions
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[0].OriginalDestinationAccountName)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"Test Account2\",\"Debt\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[0].OriginalDestinationAccountName)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Debt\",\"Test Account2\",\"Asset account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[0].OriginalDestinationAccountName)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Debt\",\"Test Account2\",\"Debt\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[0].Type)
assert.Equal(t, int64(1000), allNewTransactions[0].Amount)
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
assert.Equal(t, "Test Account2", allNewTransactions[0].OriginalDestinationAccountName)
}
func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidTransactionType(t *testing.T) {
converter := FireflyIIITransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Revenue account\",\"Test Account2\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Type,123.45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
}
@@ -222,15 +123,15 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseAccountNameAsCategoryN
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, "A expense account", allNewTransactions[0].OriginalCategoryName)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Revenue account\",\"Test Account\",\"Asset account\",\"\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Deposit,10.00,2024-09-01T12:34:56+08:00,\"A revenue account\",\"Test Account\",\"\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -246,20 +147,20 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidTimezone(t *testi
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56-10:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+00:00,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-1.00,2024-09-01T12:34:56+12:45,\"Test Account\",\"A expense account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
@@ -274,9 +175,9 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidAccountCurrency(t
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"+
"Transfer,1.23,-1.10,2024-09-01T23:59:59+08:00,USD,EUR,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
@@ -301,8 +202,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,15.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -312,8 +213,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,EUR,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -322,8 +223,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseValidForeignAmountAndC
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
assert.Equal(t, "EUR", allNewTransactions[0].OriginalDestinationAccountCurrency)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,10.00,2024-09-01T12:34:56+08:00,USD,,\"Test Account\",\"Test Account2\",\"Test Category\""), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -340,14 +241,14 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAccountCurrency
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"+
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account\",\"Test Account2\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\n"+
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account2\",\"Asset account\",\"Test Account\",\"Asset account\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,USD,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"+
"Transfer,1.23,1.10,2024-09-01T23:59:59+08:00,CNY,EUR,\"Test Account2\",\"Test Account\",\"Test Category3\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
@@ -360,12 +261,12 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseNotSupportedCurrency(t
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,,2024-09-01T00:00:00+08:00,XXX,,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,123.45,123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,currency_code,foreign_currency_code,source_name,destination_name,category\n"+
"Transfer,123.45,123.45,2024-09-01T23:59:59+08:00,USD,XXX,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
}
@@ -378,12 +279,12 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseInvalidAmount(t *testi
DefaultCurrency: "CNY",
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,category\n"+
"Withdrawal,-123 45,2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Asset account\",\"Test Account2\",\"Asset account\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,foreign_amount,date,source_name,destination_name,category\n"+
"Transfer,123.45,123 45,2024-09-01T23:59:59+08:00,\"Test Account\",\"Test Account2\",\"Test Category2\""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
}
@@ -396,8 +297,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseDescription(t *testing
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,description,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"foo bar\t#test\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -413,8 +314,8 @@ func TestFireFlyIIICsvFileConverterParseImportedData_ParseTags(t *testing.T) {
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,source_type,destination_name,destination_type,category\n"+
"Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"Asset account\",\"A expense account\",\"Expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
allNewTransactions, _, _, _, _, allNewTags, err := converter.ParseImportedData(context, user, []byte("type,amount,tags,date,source_name,destination_name,category\n"+
"Withdrawal,-123.45,\"tag1,tag2,tag3\",2024-09-01T12:34:56+08:00,\"Test Account\",\"A expense account\",\"Test Category\"\n"), 0, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -437,7 +338,7 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingFileHeader(t *testin
}
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(""), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
}
func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
@@ -450,13 +351,18 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *te
}
// Missing Time Column
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("type,amount,source_name,destination_name,category\n"+
"\"Opening balance\",123.45,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Type Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("amount,date,source_name,source_type,destination_name,destination_type,category\n"+
"123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("amount,date,source_name,destination_name,category\n"+
"123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Sub Category Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\"\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account Name Column
@@ -465,22 +371,12 @@ func TestFireFlyIIICsvFileConverterParseImportedData_MissingRequiredColumn(t *te
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Amount Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,date,source_name,source_type,destination_name,destination_type,category\n"+
"\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Initial balance account\",\"Test Account\",\"Asset account\",\n"), 0, nil, nil, nil, nil, nil)
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,date,source_name,destination_name,category\n"+
"\"Opening balance\",2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Account2 Name Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Source Account Type Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,destination_name,destination_type,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Test Account\",\"Asset account\",\"Asset account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
// Missing Destination Account Type Column
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte("type,amount,date,source_name,source_type,destination_name,category\n"+
"\"Opening balance\",123.45,2024-09-01T00:00:00+08:00,\"Initial balance for \"\"Test Account\"\"\",\"Asset account\",\"Test Account\",\n"), 0, nil, nil, nil, nil, nil)
assert.EqualError(t, err, errs.ErrMissingRequiredFieldInHeaderRow.Message)
}
@@ -2,132 +2,103 @@ package fireflyIII
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
const fireflyIIITransactionTimeColumnName = "date"
const fireflyIIITransactionTypeColumnName = "type"
const fireflyIIITransactionCategoryColumnName = "category"
const fireflyIIITransactionSourceAccountNameColumnName = "source_name"
const fireflyIIITransactionSourceAccountTypeColumnName = "source_type"
const fireflyIIITransactionCurrencyCodeColumnName = "currency_code"
const fireflyIIITransactionAmountColumnName = "amount"
const fireflyIIITransactionDestinationAccountNameColumnName = "destination_name"
const fireflyIIITransactionDestinationAccountTypeColumnName = "destination_type"
const fireflyIIITransactionForeignCurrencyCodeColumnName = "foreign_currency_code"
const fireflyIIITransactionForeignAmountColumnName = "foreign_amount"
const fireflyIIITransactionTagsColumnName = "tags"
const fireflyIIITransactionDescriptionColumnName = "description"
const fireflyIIIAssetAccountName = "Asset account"
const fireflyIIIExpenseAccountName = "Expense account"
const fireflyIIIRevenueAccountName = "Revenue account"
const fireflyIIIDebtAccountName = "Debt"
// fireflyIIITransactionDataRowParser defines the structure of firefly III transaction data row parser
type fireflyIIITransactionDataRowParser struct {
}
// GetAddedColumns returns the added columns after converting the data row
func (p *fireflyIIITransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn {
return []datatable.TransactionDataTableColumn{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE,
}
}
// Parse returns the converted transaction data row
func (p *fireflyIIITransactionDataRowParser) Parse(ctx core.Context, user *models.User, dataRow datatable.CommonDataTableRow, rowId string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
rowData = make(map[datatable.TransactionDataTableColumn]string, len(fireflyIIITransactionSupportedColumns))
func (p *fireflyIIITransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) {
rowData = make(map[datatable.TransactionDataTableColumn]string, len(data))
for column, value := range data {
rowData[column] = value
}
// use the expense and revenue account name as category names if the category name is empty
if rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == "" {
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]
} else if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] {
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
}
}
// parse long date time and timezone
dateTime, err := utils.ParseFromLongDateTimeWithTimezoneRFC3339Format(dataRow.GetData(fireflyIIITransactionTimeColumnName))
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
dateTime, err := utils.ParseFromLongDateTimeWithTimezoneRFC3339Format(rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
if err != nil {
return nil, false, errs.ErrTransactionTimeInvalid
if err != nil {
return nil, false, errs.ErrTransactionTimeInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
// parse transaction type, transaction category and amount
transactionType := dataRow.GetData(fireflyIIITransactionTypeColumnName)
sourceAccountType := dataRow.GetData(fireflyIIITransactionSourceAccountTypeColumnName)
destinationAccountType := dataRow.GetData(fireflyIIITransactionDestinationAccountTypeColumnName)
amount, err := utils.ParseAmount(utils.TrimTrailingZerosInDecimal(dataRow.GetData(fireflyIIITransactionAmountColumnName)))
if err != nil {
return nil, false, errs.ErrAmountInvalid
}
foreignAmount := amount
if dataRow.HasData(fireflyIIITransactionForeignAmountColumnName) && dataRow.GetData(fireflyIIITransactionForeignAmountColumnName) != "" {
foreignAmount, err = utils.ParseAmount(utils.TrimTrailingZerosInDecimal(dataRow.GetData(fireflyIIITransactionForeignAmountColumnName)))
// trim trailing zero in decimal
if rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
if err != nil {
return nil, false, errs.ErrAmountInvalid
}
}
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(fireflyIIITransactionCategoryColumnName)
if sourceAccountType == fireflyIIIRevenueAccountName && (destinationAccountType == fireflyIIIAssetAccountName || destinationAccountType == fireflyIIIDebtAccountName) { // income
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME]
// if the category is empty, use the source account (revenue account) name as the category name
if rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == "" {
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(fireflyIIITransactionSourceAccountNameColumnName)
}
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(fireflyIIITransactionDestinationAccountNameColumnName)
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
} else if (sourceAccountType == fireflyIIIAssetAccountName || sourceAccountType == fireflyIIIDebtAccountName) && destinationAccountType == fireflyIIIExpenseAccountName { // expense
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE]
// if the category is empty, use the destination account (expense account) name as the category name
if rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == "" {
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = dataRow.GetData(fireflyIIITransactionDestinationAccountNameColumnName)
}
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(fireflyIIITransactionSourceAccountNameColumnName)
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
} else if transactionType == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] { // opening balance
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE]
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(fireflyIIITransactionDestinationAccountNameColumnName)
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
} else if (sourceAccountType == fireflyIIIAssetAccountName || sourceAccountType == fireflyIIIDebtAccountName) && (destinationAccountType == fireflyIIIAssetAccountName || destinationAccountType == fireflyIIIDebtAccountName) { // transfer
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_TRANSFER]
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = dataRow.GetData(fireflyIIITransactionSourceAccountNameColumnName)
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = dataRow.GetData(fireflyIIITransactionDestinationAccountNameColumnName)
if transactionType == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] {
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] {
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount)
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-foreignAmount)
} else {
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(foreignAmount)
}
} else {
log.Errorf(ctx, "[fireflyiii_transaction_data_row_parser.Parse] cannot detect transaction type, source account type is \"%s\", destination account type is \"%s\", Firefly III transaction type is \"%s\"", sourceAccountType, destinationAccountType, transactionType)
return nil, false, errs.ErrTransactionTypeInvalid
}
// parse account currency
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = dataRow.GetData(fireflyIIITransactionCurrencyCodeColumnName)
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = dataRow.GetData(fireflyIIITransactionForeignCurrencyCodeColumnName)
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
if err != nil {
return nil, false, errs.ErrAmountInvalid
}
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_EXPENSE] {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(-amount)
} else {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amount)
}
} else {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]
}
// the related account currency field is foreign currency in firefly III actually
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] == "" && rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] != "" {
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] == "" {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY]
}
// parse tags / description
rowData[datatable.TRANSACTION_DATA_TABLE_TAGS] = dataRow.GetData(fireflyIIITransactionTagsColumnName)
rowData[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = dataRow.GetData(fireflyIIITransactionDescriptionColumnName)
// the destination account of modify balance transaction in firefly III is the asset account
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_MODIFY_BALANCE] {
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
}
// the destination account of income transaction in firefly III is the asset account
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == fireflyIIITransactionTypeNameMapping[models.TRANSACTION_TYPE_INCOME] {
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]
}
return rowData, true, nil
}
// createFireflyIIITransactionDataRowParser returns firefly III transaction data row parser
func createFireflyIIITransactionDataRowParser() datatable.CommonTransactionDataRowParser {
func createFireflyIIITransactionDataRowParser() datatable.TransactionDataRowParser {
return &fireflyIIITransactionDataRowParser{}
}
@@ -69,6 +69,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor
return alipay.AlipayAppTransactionDataCsvFileImporter, nil
} else if fileType == "alipay_web_csv" {
return alipay.AlipayWebTransactionDataCsvFileImporter, nil
} else if fileType == "wechat_pay_app_xlsx" {
return wechat.WeChatPayTransactionDataXlsxFileImporter, nil
} else if fileType == "wechat_pay_app_csv" {
return wechat.WeChatPayTransactionDataCsvFileImporter, nil
} else {
@@ -2,14 +2,12 @@ package wechat
import (
"bytes"
"encoding/csv"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"io"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
csvdatatable "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -17,22 +15,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
var wechatPayTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
}
var wechatPayTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_INCOME: "收入",
models.TRANSACTION_TYPE_EXPENSE: "支出",
models.TRANSACTION_TYPE_TRANSFER: "/",
}
// wechatPayTransactionDataCsvFileImporter defines the structure of wechatPay csv importer for transaction data
type wechatPayTransactionDataCsvFileImporter struct {
fileHeaderLineBeginning string
@@ -49,7 +31,13 @@ func (c *wechatPayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Con
fallback := unicode.UTF8.NewDecoder()
reader := transform.NewReader(bytes.NewReader(data), unicode.BOMOverride(fallback))
dataTable, err := c.createNewWeChatPayBasicDataTable(ctx, reader)
csvDataTable, err := csv.CreateNewCsvBasicDataTable(ctx, reader, false)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTable, err := createNewWeChatPayTransactionBasicDataTable(ctx, csvDataTable)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
@@ -72,78 +60,3 @@ func (c *wechatPayTransactionDataCsvFileImporter) ParseImportedData(ctx core.Con
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
func (c *wechatPayTransactionDataCsvFileImporter) createNewWeChatPayBasicDataTable(ctx core.Context, reader io.Reader) (datatable.BasicDataTable, error) {
csvReader := csv.NewReader(reader)
csvReader.FieldsPerRecord = -1
allOriginalLines := make([][]string, 0)
hasFileHeader := false
foundContentBeforeDataHeaderLine := false
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayBasicDataTable] cannot parse wechat pay csv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if !hasFileHeader {
if len(items) <= 0 {
continue
} else if strings.Index(items[0], wechatPayTransactionDataCsvFileHeader) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayBasicDataTable] read unexpected line before read file header, line content is %s", strings.Join(items, ","))
continue
}
}
if !foundContentBeforeDataHeaderLine {
if len(items) <= 0 {
continue
} else if strings.Index(items[0], wechatPayTransactionDataHeaderStartContentBeginning) == 0 {
foundContentBeforeDataHeaderLine = true
continue
} else {
continue
}
}
if foundContentBeforeDataHeaderLine {
if len(items) <= 0 {
continue
}
for i := 0; i < len(items); i++ {
items[i] = strings.Trim(items[i], " ")
}
if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) {
log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayBasicDataTable] cannot parse row \"index:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", len(allOriginalLines), len(items), len(allOriginalLines[0]))
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
allOriginalLines = append(allOriginalLines, items)
}
}
if !hasFileHeader || !foundContentBeforeDataHeaderLine {
return nil, errs.ErrInvalidFileHeader
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[wechat_pay_transaction_data_csv_file_importer.createNewWeChatPayBasicDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
dataTable := csvdatatable.CreateNewCustomCsvBasicDataTable(allOriginalLines)
return dataTable, nil
}
@@ -0,0 +1,75 @@
package wechat
import (
"strings"
"github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
)
func createNewWeChatPayTransactionBasicDataTable(ctx core.Context, originalDataTable datatable.BasicDataTable) (datatable.BasicDataTable, error) {
iterator := originalDataTable.DataRowIterator()
allOriginalLines := make([][]string, 0)
hasFileHeader := false
foundContentBeforeDataHeaderLine := false
for iterator.HasNext() {
row := iterator.Next()
if !hasFileHeader {
if row.ColumnCount() <= 0 {
continue
} else if strings.Index(row.GetData(0), wechatPayTransactionDataCsvFileHeader) == 0 {
hasFileHeader = true
continue
} else {
log.Warnf(ctx, "[wechat_pay_transaction_data_extrator.createNewWeChatPayTransactionBasicDataTable] read unexpected line in row \"%s\" before read file header", iterator.CurrentRowId())
continue
}
}
if !foundContentBeforeDataHeaderLine {
if row.ColumnCount() <= 0 {
continue
} else if strings.Index(row.GetData(0), wechatPayTransactionDataHeaderStartContentBeginning) == 0 {
foundContentBeforeDataHeaderLine = true
continue
} else {
continue
}
}
if foundContentBeforeDataHeaderLine {
if row.ColumnCount() <= 0 {
continue
}
items := make([]string, row.ColumnCount())
for i := 0; i < row.ColumnCount(); i++ {
items[i] = strings.Trim(row.GetData(i), " ")
}
if len(allOriginalLines) > 0 && len(items) < len(allOriginalLines[0]) {
log.Errorf(ctx, "[wechat_pay_transaction_data_extrator.createNewWeChatPayTransactionBasicDataTable] cannot parse row \"%s\", because may missing some columns (column count %d in data row is less than header column count %d)", iterator.CurrentRowId(), len(items), len(allOriginalLines[0]))
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
allOriginalLines = append(allOriginalLines, items)
}
}
if !hasFileHeader || !foundContentBeforeDataHeaderLine {
return nil, errs.ErrInvalidFileHeader
}
if len(allOriginalLines) < 2 {
log.Errorf(ctx, "[wechat_pay_transaction_data_extrator.createNewWeChatPayTransactionBasicDataTable] cannot parse import data, because data table row count is less 1")
return nil, errs.ErrNotFoundTransactionDataInFile
}
return csv.CreateNewCustomCsvBasicDataTable(allOriginalLines, true), nil
}
@@ -29,6 +29,22 @@ const wechatPayTransactionDataCategoryTransferFromWeChatWallet = "零钱提现"
const wechatPayTransactionDataStatusRefundName = "退款"
var wechatPayTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
}
var wechatPayTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_INCOME: "收入",
models.TRANSACTION_TYPE_EXPENSE: "支出",
models.TRANSACTION_TYPE_TRANSFER: "/",
}
// weChatPayTransactionDataRowParser defines the structure of wechat pay transaction data row parser
type weChatPayTransactionDataRowParser struct {
existedOriginalDataColumns map[string]bool
@@ -0,0 +1,53 @@
package wechat
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// wechatPayTransactionDataXlsxFileImporter defines the structure of wechatPay xlsx importer for transaction data
type wechatPayTransactionDataXlsxFileImporter struct {
dataHeaderStartContentBeginning string
}
// Initialize a webchat pay transaction data xlsx file importer singleton instance
var (
WeChatPayTransactionDataXlsxFileImporter = &wechatPayTransactionDataXlsxFileImporter{}
)
// ParseImportedData returns the imported data by parsing the wechat pay transaction csv data
func (c *wechatPayTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
xlsxDataTable, err := excel.CreateNewExcelOOXMLFileBasicDataTable(data, false)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTable, err := createNewWeChatPayTransactionBasicDataTable(ctx, xlsxDataTable)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
commonDataTable := datatable.CreateNewCommonDataTableFromBasicDataTable(dataTable)
if !commonDataTable.HasColumn(wechatPayTransactionTimeColumnName) ||
!commonDataTable.HasColumn(wechatPayTransactionCategoryColumnName) ||
!commonDataTable.HasColumn(wechatPayTransactionTypeColumnName) ||
!commonDataTable.HasColumn(wechatPayTransactionAmountColumnName) ||
!commonDataTable.HasColumn(wechatPayTransactionStatusColumnName) {
log.Errorf(ctx, "[wechat_pay_transaction_data_xlsx_file_importer.ParseImportedData] cannot parse wechat pay xlsx data, because missing essential columns in header row")
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
}
transactionRowParser := createWeChatPayTransactionDataRowParser(dataTable.HeaderColumnNames())
transactionDataTable := datatable.CreateNewTransactionDataTableFromCommonDataTable(commonDataTable, wechatPayTransactionSupportedColumns, transactionRowParser)
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(wechatPayTransactionTypeNameMapping)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
+57
View File
@@ -0,0 +1,57 @@
package core
import "fmt"
// CalendarDisplayType represents calendar display type
type CalendarDisplayType byte
// Calendar Display Type
const (
CALENDAR_DISPLAY_TYPE_DEFAULT CalendarDisplayType = 0
CALENDAR_DISPLAY_TYPE_GREGORAIN CalendarDisplayType = 1
CALENDAR_DISPLAY_TYPE_BUDDHIST CalendarDisplayType = 2
CALENDAR_DISPLAY_TYPE_INVALID CalendarDisplayType = 255
)
// String returns a textual representation of the calendar display type enum
func (f CalendarDisplayType) String() string {
switch f {
case CALENDAR_DISPLAY_TYPE_DEFAULT:
return "Default"
case CALENDAR_DISPLAY_TYPE_GREGORAIN:
return "Gregorian"
case CALENDAR_DISPLAY_TYPE_BUDDHIST:
return "Buddhist"
case CALENDAR_DISPLAY_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// DateDisplayType represents date display type
type DateDisplayType byte
// Date Display Type
const (
DATE_DISPLAY_TYPE_DEFAULT DateDisplayType = 0
DATE_DISPLAY_TYPE_GREGORAIN DateDisplayType = 1
DATE_DISPLAY_TYPE_BUDDHIST DateDisplayType = 2
DATE_DISPLAY_TYPE_INVALID DateDisplayType = 255
)
// String returns a textual representation of the date display type enum
func (f DateDisplayType) String() string {
switch f {
case DATE_DISPLAY_TYPE_DEFAULT:
return "Default"
case DATE_DISPLAY_TYPE_GREGORAIN:
return "Gregorian"
case DATE_DISPLAY_TYPE_BUDDHIST:
return "Buddhist"
case DATE_DISPLAY_TYPE_INVALID:
return "Invalid"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
+3
View File
@@ -1,7 +1,10 @@
package core
import "context"
// Context is the base context of ezBookkeeping
type Context interface {
context.Context
GetContextId() string
GetClientLocale() string
}
+41
View File
@@ -3,6 +3,7 @@ package core
import (
"net"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -23,6 +24,11 @@ const RemoteClientPortHeader = "X-Real-Port"
// ClientTimezoneOffsetHeaderName represents the header name of client timezone offset
const ClientTimezoneOffsetHeaderName = "X-Timezone-Offset"
const tokenHeaderName = "Authorization"
const tokenHeaderValuePrefix = "bearer "
const tokenQueryStringParam = "token"
const tokenCookieParam = "ebk_auth_token"
// WebContext represents the request and response context
type WebContext struct {
*gin.Context
@@ -118,6 +124,41 @@ func (c *WebContext) GetCurrentUid() int64 {
return claims.Uid
}
// GetTokenStringFromHeader returns the token string from the request header
func (c *WebContext) GetTokenStringFromHeader() string {
tokenHeader := c.GetHeader(tokenHeaderName)
if len(tokenHeader) < 7 || !strings.EqualFold(tokenHeader[:7], tokenHeaderValuePrefix) {
return ""
}
return tokenHeader[7:]
}
// GetTokenStringFromQueryString returns the token string from the request query string
func (c *WebContext) GetTokenStringFromQueryString() string {
return c.Query(tokenQueryStringParam)
}
// GetTokenStringFromCookie returns the token string from the request cookie
func (c *WebContext) GetTokenStringFromCookie() string {
tokenCookie, err := c.Cookie(tokenCookieParam)
if err != nil {
return ""
}
return tokenCookie
}
func (c *WebContext) SetTokenStringToCookie(token string, tokenExpiredTime int, path string) {
if token != "" {
c.SetCookie(tokenCookieParam, token, tokenExpiredTime, path, "", false, true)
} else {
c.SetCookie(tokenCookieParam, "", -1, path, "", false, true)
}
}
// GetClientLocale returns the client locale name
func (c *WebContext) GetClientLocale() string {
value := c.GetHeader(AcceptLanguageHeaderName)
+41 -4
View File
@@ -4,6 +4,40 @@ import (
"fmt"
)
// NumeralSystem represents the type of numeral system
type NumeralSystem byte
// Numeral System
const (
NUMERAL_SYSTEM_DEFAULT NumeralSystem = 0
NUMERAL_SYSTEM_WESTERN_ARABIC_NUMERALS NumeralSystem = 1
NUMERAL_SYSTEM_EASTERN_ARABIC_NUMERALS NumeralSystem = 2
NUMERAL_SYSTEM_PERSIAN_DIGITS NumeralSystem = 3
NUMERAL_SYSTEM_BURMESE_NUMERALS NumeralSystem = 4
NUMERAL_SYSTEM_DEVANAGARI_NUMERALS NumeralSystem = 5
NUMERAL_SYSTEM_INVALID NumeralSystem = 255
)
// String returns a textual representation of the decimal separator enum
func (f NumeralSystem) String() string {
switch f {
case NUMERAL_SYSTEM_DEFAULT:
return "Default"
case NUMERAL_SYSTEM_WESTERN_ARABIC_NUMERALS:
return "Western Arabic Numerals"
case NUMERAL_SYSTEM_EASTERN_ARABIC_NUMERALS:
return "Eastern Arabic Numerals"
case NUMERAL_SYSTEM_PERSIAN_DIGITS:
return "Persian Digits"
case NUMERAL_SYSTEM_BURMESE_NUMERALS:
return "Burmese Numerals"
case NUMERAL_SYSTEM_DEVANAGARI_NUMERALS:
return "Devanagari Numerals"
default:
return fmt.Sprintf("Invalid(%d)", int(f))
}
}
// DecimalSeparator represents the type of decimal separator
type DecimalSeparator byte
@@ -69,10 +103,11 @@ type DigitGroupingType byte
// Digit Grouping Type
const (
DIGIT_GROUPING_TYPE_DEFAULT DigitGroupingType = 0
DIGIT_GROUPING_TYPE_NONE DigitGroupingType = 1
DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR DigitGroupingType = 2
DIGIT_GROUPING_TYPE_INVALID DigitGroupingType = 255
DIGIT_GROUPING_TYPE_DEFAULT DigitGroupingType = 0
DIGIT_GROUPING_TYPE_NONE DigitGroupingType = 1
DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR DigitGroupingType = 2
DIGIT_GROUPING_TYPE_INDIAN_NUMBER_GROUPING DigitGroupingType = 3
DIGIT_GROUPING_TYPE_INVALID DigitGroupingType = 255
)
// String returns a textual representation of the digit grouping type enum
@@ -84,6 +119,8 @@ func (d DigitGroupingType) String() string {
return "None"
case DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR:
return "Thousands Separator"
case DIGIT_GROUPING_TYPE_INDIAN_NUMBER_GROUPING:
return "Indian Number Grouping"
case DIGIT_GROUPING_TYPE_INVALID:
return "Invalid"
default:
+1 -1
View File
@@ -95,7 +95,7 @@ func TestCronJobSchedulerContainerRepeatRun(t *testing.T) {
InMemoryDuplicateCheckerCleanupIntervalDuration: 60 * time.Second,
})
duplicatechecker.Container.Current = checker
duplicatechecker.SetDuplicateChecker(checker)
container := &CronJobSchedulerContainer{
allJobsMap: make(map[string]*CronJob),
+1 -1
View File
@@ -22,7 +22,7 @@ func (j *CronJob) doRun() {
start := time.Now()
c := core.NewCronJobContext(j.Name, j.Period.GetInterval())
if duplicatechecker.Container.Current != nil {
if duplicatechecker.Container.IsEnabled() {
localAddr, err := utils.GetLocalIPAddressesString()
if err != nil {
@@ -9,7 +9,7 @@ import (
// DuplicateCheckerContainer contains the current duplicate checker
type DuplicateCheckerContainer struct {
Current DuplicateChecker
current DuplicateChecker
}
// Initialize a duplicate checker container singleton instance
@@ -21,7 +21,7 @@ var (
func InitializeDuplicateChecker(config *settings.Config) error {
if config.DuplicateCheckerType == settings.InMemoryDuplicateCheckerType {
checker, err := NewInMemoryDuplicateChecker(config)
Container.Current = checker
Container.current = checker
return err
}
@@ -29,37 +29,75 @@ func InitializeDuplicateChecker(config *settings.Config) error {
return errs.ErrInvalidDuplicateCheckerType
}
// SetDuplicateChecker sets the current duplicate checker
func SetDuplicateChecker(checker DuplicateChecker) {
Container.current = checker
}
// IsEnabled returns whether the duplicate checker is enabled
func (c *DuplicateCheckerContainer) IsEnabled() bool {
return c.current != nil
}
// GetSubmissionRemark returns whether the same submission has been processed and related remark by the current duplicate checker
func (c *DuplicateCheckerContainer) GetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) (bool, string) {
return c.Current.GetSubmissionRemark(checkerType, uid, identification)
if c.current == nil {
return false, ""
}
return c.current.GetSubmissionRemark(checkerType, uid, identification)
}
// SetSubmissionRemark saves the identification and remark by the current duplicate checker
func (c *DuplicateCheckerContainer) SetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string, remark string) {
c.Current.SetSubmissionRemark(checkerType, uid, identification, remark)
if c.current == nil {
return
}
c.current.SetSubmissionRemark(checkerType, uid, identification, remark)
}
// RemoveSubmissionRemark removes the identification and remark by the current duplicate checker
func (c *DuplicateCheckerContainer) RemoveSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string) {
c.Current.RemoveSubmissionRemark(checkerType, uid, identification)
if c.current == nil {
return
}
c.current.RemoveSubmissionRemark(checkerType, uid, identification)
}
// GetOrSetCronJobRunningInfo returns the running info when the cron job is running or saves the running info by the current duplicate checker
func (c *DuplicateCheckerContainer) GetOrSetCronJobRunningInfo(jobName string, runningInfo string, runningInterval time.Duration) (bool, string) {
return c.Current.GetOrSetCronJobRunningInfo(jobName, runningInfo, runningInterval)
if c.current == nil {
return false, ""
}
return c.current.GetOrSetCronJobRunningInfo(jobName, runningInfo, runningInterval)
}
// RemoveCronJobRunningInfo removes the running info of the cron job by the current duplicate checker
func (c *DuplicateCheckerContainer) RemoveCronJobRunningInfo(jobName string) {
c.Current.RemoveCronJobRunningInfo(jobName)
if c.current == nil {
return
}
c.current.RemoveCronJobRunningInfo(jobName)
}
// GetFailureCount returns the failure count of the specified failure key
func (c *DuplicateCheckerContainer) GetFailureCount(failureKey string) uint32 {
return c.Current.GetFailureCount(failureKey)
if c.current == nil {
return 0
}
return c.current.GetFailureCount(failureKey)
}
// IncreaseFailureCount increases the failure count of the specified failure key
func (c *DuplicateCheckerContainer) IncreaseFailureCount(failureKey string) uint32 {
return c.Current.IncreaseFailureCount(failureKey)
if c.current == nil {
return 0
}
return c.current.IncreaseFailureCount(failureKey)
}
@@ -1,13 +1,15 @@
package exchangerates
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// ExchangeRatesDataSourceContainer contains the current exchange rates data source
type ExchangeRatesDataSourceContainer struct {
Current ExchangeRatesDataSource
current ExchangeRatesDataSource
}
// Initialize a exchange rates data source container singleton instance
@@ -18,60 +20,69 @@ var (
// InitializeExchangeRatesDataSource initializes the current exchange rates data source according to the config
func InitializeExchangeRatesDataSource(config *settings.Config) error {
if config.ExchangeRatesDataSource == settings.ReserveBankOfAustraliaDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&ReserveBankOfAustraliaDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&ReserveBankOfAustraliaDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.BankOfCanadaDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&BankOfCanadaDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&BankOfCanadaDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&CzechNationalBankDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&CzechNationalBankDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.DanmarksNationalbankDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&DanmarksNationalbankDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&DanmarksNationalbankDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.EuroCentralBankDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&EuroCentralBankDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&EuroCentralBankDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.NationalBankOfGeorgiaDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&NationalBankOfGeorgiaDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&NationalBankOfGeorgiaDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.CentralBankOfHungaryDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&CentralBankOfHungaryDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&CentralBankOfHungaryDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.BankOfIsraelDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&BankOfIsraelDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&BankOfIsraelDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.CentralBankOfMyanmarDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&CentralBankOfMyanmarDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&CentralBankOfMyanmarDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.NorgesBankDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&NorgesBankDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&NorgesBankDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.NationalBankOfPolandDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&NationalBankOfPolandDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&NationalBankOfPolandDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.NationalBankOfRomaniaDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&NationalBankOfRomaniaDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&NationalBankOfRomaniaDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.BankOfRussiaDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&BankOfRussiaDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&BankOfRussiaDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.SwissNationalBankDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&SwissNationalBankDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&SwissNationalBankDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.NationalBankOfUkraineDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&NationalBankOfUkraineDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&NationalBankOfUkraineDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.CentralBankOfUzbekistanDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&CentralBankOfUzbekistanDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&CentralBankOfUzbekistanDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.InternationalMonetaryFundDataSource {
Container.Current = newCommonHttpExchangeRatesDataSource(&InternationalMonetaryFundDataSource{})
Container.current = newCommonHttpExchangeRatesDataSource(&InternationalMonetaryFundDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.UserCustomExchangeRatesDataSource {
Container.Current = newUserCustomExchangeRatesDataSource()
Container.current = newUserCustomExchangeRatesDataSource()
return nil
}
return errs.ErrInvalidExchangeRatesDataSource
}
// GetLatestExchangeRates returns the latest exchange rates data from the current exchange rates data source
func (e *ExchangeRatesDataSourceContainer) GetLatestExchangeRates(c core.Context, uid int64, currentConfig *settings.Config) (*models.LatestExchangeRateResponse, error) {
if Container.current == nil {
return nil, errs.ErrInvalidExchangeRatesDataSource
}
return e.current.GetLatestExchangeRates(c, uid, currentConfig)
}
@@ -326,15 +326,12 @@ func executeLatestExchangeRateHandler(t *testing.T, dataSourceType string) *mode
err := InitializeExchangeRatesDataSource(config)
assert.Nil(t, err)
dataSource := Container.Current
assert.NotNil(t, dataSource)
ginContext, _ := gin.CreateTestContext(httptest.NewRecorder())
context := &core.WebContext{
Context: ginContext,
}
exchangeRateResponse, err := dataSource.GetLatestExchangeRates(context, context.GetCurrentUid(), config)
exchangeRateResponse, err := Container.GetLatestExchangeRates(context, context.GetCurrentUid(), config)
assert.Nil(t, err)
assert.NotNil(t, exchangeRateResponse)
+3
View File
@@ -21,6 +21,9 @@ var AllLanguages = map[string]*LocaleInfo{
"ja": {
Content: ja,
},
"nl": {
Content: nl,
},
"pt-BR": {
Content: ptBR,
},
+30
View File
@@ -0,0 +1,30 @@
package locales
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
)
var nl = &LocaleTextItems{
DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,
},
DataConverterTextItems: &DataConverterTextItems{
Alipay: "Alipay",
WeChatWallet: "Wallet",
},
VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "Verifieer e-mail",
SalutationFormat: "Hallo %s,",
DescriptionAboveBtn: "Klik op de onderstaande link om je e-mailadres te bevestigen.",
VerifyEmail: "Verifieer e-mail",
DescriptionBelowBtnFormat: "Als je geen %s account hebt aangemaakt, kun je deze e-mail negeren. Als je niet op de bovenstaande link kunt klikken, kopieer dan de URL hierboven en plak deze in je browser. De verificatielink verloopt na %v minuten.",
},
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "Wachtwoord opnieuw instellen",
SalutationFormat: "Hallo %s,",
DescriptionAboveBtn: "We hebben onlangs een verzoek ontvangen om je wachtwoord opnieuw in te stellen. Klik op de onderstaande link om je wachtwoord te resetten.",
ResetPassword: "Wachtwoord opnieuw instellen",
DescriptionBelowBtnFormat: "Als je geen verzoek hebt gedaan om je wachtwoord te resetten, kun je deze e-mail negeren. Als je niet op de bovenstaande link kunt klikken, kopieer dan de URL hierboven en plak deze in je browser. De link voor het opnieuw instellen van het wachtwoord verloopt na %v minuten.",
},
}
+10 -5
View File
@@ -1,12 +1,13 @@
package mail
import (
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// MailerContainer contains the current mailer
type MailerContainer struct {
Current Mailer
current Mailer
}
// Initialize a mailer container singleton instance
@@ -17,7 +18,7 @@ var (
// InitializeMailer initializes the current mailer according to the config
func InitializeMailer(config *settings.Config) error {
if !config.EnableSMTP {
Container.Current = nil
Container.current = nil
return nil
}
@@ -27,11 +28,15 @@ func InitializeMailer(config *settings.Config) error {
return err
}
Container.Current = mailer
Container.current = mailer
return nil
}
// SendMail sends an email according to argument
func (u *MailerContainer) SendMail(message *MailMessage) error {
return u.Current.SendMail(message)
func (m *MailerContainer) SendMail(message *MailMessage) error {
if m.current == nil {
return errs.ErrSMTPServerNotEnabled
}
return m.current.SendMail(message)
}
@@ -68,13 +68,7 @@ func (h *mcpQueryLatestExchangeRatesToolHandler) Handle(c *core.WebContext, call
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
}
dataSource := exchangerates.Container.Current
if dataSource == nil {
return nil, nil, errs.ErrInvalidExchangeRatesDataSource
}
exchangeRateResponse, err := dataSource.GetLatestExchangeRates(c, user.Uid, currentConfig)
exchangeRateResponse, err := exchangerates.Container.GetLatestExchangeRates(c, user.Uid, currentConfig)
if err != nil {
return nil, nil, err
@@ -5,15 +5,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const tokenCookieParam = "ebk_auth_token"
// AmapApiProxyAuthCookie adds amap api proxy auth cookie to cookies in response
func AmapApiProxyAuthCookie(c *core.WebContext, config *settings.Config) {
token := c.GetTextualToken()
if token != "" {
c.SetCookie(tokenCookieParam, token, int(config.TokenExpiredTime), "/_AMapService", "", false, true)
} else {
c.SetCookie(tokenCookieParam, "", -1, "/_AMapService", "", false, true)
}
c.SetTokenStringToCookie(token, int(config.TokenExpiredTime), "/_AMapService")
}
+11 -5
View File
@@ -20,8 +20,6 @@ const (
TOKEN_SOURCE_TYPE_COOKIE TokenSourceType = 3
)
const tokenQueryStringParam = "token"
// JWTAuthorization verifies whether current request is valid by jwt token in header
func JWTAuthorization(c *core.WebContext) {
jwtAuthorization(c, TOKEN_SOURCE_TYPE_HEADER)
@@ -159,11 +157,19 @@ func getTokenClaims(c *core.WebContext, source TokenSourceType) (*core.UserToken
}
func parseToken(c *core.WebContext, source TokenSourceType) (*jwt.Token, *core.UserTokenClaims, error) {
tokenString := ""
if source == TOKEN_SOURCE_TYPE_ARGUMENT {
return services.Tokens.ParseTokenByArgument(c, tokenQueryStringParam)
tokenString = c.GetTokenStringFromQueryString()
} else if source == TOKEN_SOURCE_TYPE_COOKIE {
return services.Tokens.ParseTokenByCookie(c, tokenCookieParam)
tokenString = c.GetTokenStringFromCookie()
} else { // if source == TOKEN_SOURCE_TYPE_HEADER
tokenString = c.GetTokenStringFromHeader()
}
return services.Tokens.ParseTokenByHeader(c)
if tokenString == "" {
return nil, nil, errs.ErrTokenIsEmpty
}
return services.Tokens.ParseToken(c, tokenString)
}
+1 -6
View File
@@ -11,12 +11,7 @@ const requestIdHeader = "X-Request-ID"
// RequestId generates a new request id and add it to context and response header
func RequestId(config *settings.Config) core.MiddlewareHandlerFunc {
return func(c *core.WebContext) {
if requestid.Container.Current == nil {
c.Next()
return
}
requestId := requestid.Container.Current.GenerateRequestId(c.ClientIP(), c.ClientPort())
requestId := requestid.Container.GenerateRequestId(c.ClientIP(), c.ClientPort())
c.SetContextId(requestId)
if config.EnableRequestIdHeader {
+10
View File
@@ -45,6 +45,16 @@ var liabilityAccountCategory = map[AccountCategory]bool{
ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT: false,
}
// IsAsset returns whether the account category is an asset category
func (c AccountCategory) IsAsset() bool {
return assetAccountCategory[c]
}
// IsLiability returns whether the account category is a liability category
func (c AccountCategory) IsLiability() bool {
return liabilityAccountCategory[c]
}
// AccountType represents account type
type AccountType byte
+30
View File
@@ -120,6 +120,13 @@ type Transaction struct {
DeletedUnixTime int64
}
// TransactionWithAccountBalance represents a transaction item with account balance
type TransactionWithAccountBalance struct {
*Transaction
AccountOpeningBalance int64
AccountClosingBalance int64
}
// TransactionGeoLocationRequest represents all parameters of transaction geographic location info update request
type TransactionGeoLocationRequest struct {
Latitude float64 `json:"latitude" binding:"required"`
@@ -222,6 +229,13 @@ type TransactionListInMonthByPageRequest struct {
TrimTag bool `form:"trim_tag"`
}
// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request
type TransactionReconciliationStatementRequest struct {
AccountId int64 `form:"account_id,string" binding:"required,min=1"`
StartTime int64 `form:"start_time"`
EndTime int64 `form:"end_time"`
}
// TransactionStatisticRequest represents all parameters of transaction statistic request
type TransactionStatisticRequest struct {
StartTime int64 `form:"start_time" binding:"min=0"`
@@ -322,6 +336,22 @@ type TransactionInfoPageWrapperResponse2 struct {
TotalCount int64 `json:"totalCount"`
}
// TransactionReconciliationStatementResponseItem represents a transaction reconciliation statement response
type TransactionReconciliationStatementResponseItem struct {
*TransactionInfoResponse
AccountOpeningBalance int64 `json:"accountOpeningBalance"`
AccountClosingBalance int64 `json:"accountClosingBalance"`
}
// TransactionReconciliationStatementResponse represents the response of all transaction reconciliation statement response
type TransactionReconciliationStatementResponse struct {
Transactions []*TransactionReconciliationStatementResponseItem `json:"transactions"`
TotalInflows int64 `json:"totalInflows"`
TotalOutflows int64 `json:"totalOutflows"`
OpeningBalance int64 `json:"openingBalance"`
ClosingBalance int64 `json:"closingBalance"`
}
// TransactionStatisticResponse represents transaction statistic response
type TransactionStatisticResponse struct {
StartTime int64 `json:"startTime"`
+17 -5
View File
@@ -95,15 +95,18 @@ type User struct {
DefaultCurrency string `xorm:"VARCHAR(3) NOT NULL"`
FirstDayOfWeek core.WeekDay `xorm:"TINYINT NOT NULL"`
FiscalYearStart core.FiscalYearStart `xorm:"SMALLINT"`
CalendarDisplayType core.CalendarDisplayType `xorm:"TINYINT"`
DateDisplayType core.DateDisplayType `xorm:"TINYINT"`
LongDateFormat core.LongDateFormat `xorm:"TINYINT"`
ShortDateFormat core.ShortDateFormat `xorm:"TINYINT"`
LongTimeFormat core.LongTimeFormat `xorm:"TINYINT"`
ShortTimeFormat core.ShortTimeFormat `xorm:"TINYINT"`
FiscalYearFormat core.FiscalYearFormat `xorm:"TINYINT"`
CurrencyDisplayType core.CurrencyDisplayType `xorm:"TINYINT"`
NumeralSystem core.NumeralSystem `xorm:"TINYINT"`
DecimalSeparator core.DecimalSeparator `xorm:"TINYINT"`
DigitGroupingSymbol core.DigitGroupingSymbol `xorm:"TINYINT"`
DigitGrouping core.DigitGroupingType `xorm:"TINYINT"`
CurrencyDisplayType core.CurrencyDisplayType `xorm:"TINYINT"`
CoordinateDisplayType core.CoordinateDisplayType `xorm:"TINYINT"`
ExpenseAmountColor AmountColorType `xorm:"TINYINT"`
IncomeAmountColor AmountColorType `xorm:"TINYINT"`
@@ -130,15 +133,18 @@ type UserBasicInfo struct {
DefaultCurrency string `json:"defaultCurrency"`
FirstDayOfWeek core.WeekDay `json:"firstDayOfWeek"`
FiscalYearStart core.FiscalYearStart `json:"fiscalYearStart"`
CalendarDisplayType core.CalendarDisplayType `json:"calendarDisplayType"`
DateDisplayType core.DateDisplayType `json:"dateDisplayType"`
LongDateFormat core.LongDateFormat `json:"longDateFormat"`
ShortDateFormat core.ShortDateFormat `json:"shortDateFormat"`
LongTimeFormat core.LongTimeFormat `json:"longTimeFormat"`
ShortTimeFormat core.ShortTimeFormat `json:"shortTimeFormat"`
FiscalYearFormat core.FiscalYearFormat `json:"fiscalYearFormat"`
CurrencyDisplayType core.CurrencyDisplayType `json:"currencyDisplayType"`
NumeralSystem core.NumeralSystem `json:"numeralSystem"`
DecimalSeparator core.DecimalSeparator `json:"decimalSeparator"`
DigitGroupingSymbol core.DigitGroupingSymbol `json:"digitGroupingSymbol"`
DigitGrouping core.DigitGroupingType `json:"digitGrouping"`
CurrencyDisplayType core.CurrencyDisplayType `json:"currencyDisplayType"`
CoordinateDisplayType core.CoordinateDisplayType `json:"coordinateDisplayType"`
ExpenseAmountColor AmountColorType `json:"expenseAmountColor"`
IncomeAmountColor AmountColorType `json:"incomeAmountColor"`
@@ -193,15 +199,18 @@ type UserProfileUpdateRequest struct {
DefaultCurrency string `json:"defaultCurrency" binding:"omitempty,len=3,validCurrency"`
FirstDayOfWeek *core.WeekDay `json:"firstDayOfWeek" binding:"omitempty,min=0,max=6"`
FiscalYearStart *core.FiscalYearStart `json:"fiscalYearStart" binding:"omitempty,validFiscalYearStart"`
CalendarDisplayType *core.CalendarDisplayType `json:"calendarDisplayType" binding:"omitempty,min=0,max=2"`
DateDisplayType *core.DateDisplayType `json:"dateDisplayType" binding:"omitempty,min=0,max=2"`
LongDateFormat *core.LongDateFormat `json:"longDateFormat" binding:"omitempty,min=0,max=3"`
ShortDateFormat *core.ShortDateFormat `json:"shortDateFormat" binding:"omitempty,min=0,max=3"`
LongTimeFormat *core.LongTimeFormat `json:"longTimeFormat" binding:"omitempty,min=0,max=3"`
ShortTimeFormat *core.ShortTimeFormat `json:"shortTimeFormat" binding:"omitempty,min=0,max=3"`
FiscalYearFormat *core.FiscalYearFormat `json:"fiscalYearFormat" binding:"omitempty,min=0,max=5"`
CurrencyDisplayType *core.CurrencyDisplayType `json:"currencyDisplayType" binding:"omitempty,min=0,max=11"`
NumeralSystem *core.NumeralSystem `json:"numeralSystem" binding:"omitempty,min=0,max=5"`
DecimalSeparator *core.DecimalSeparator `json:"decimalSeparator" binding:"omitempty,min=0,max=3"`
DigitGroupingSymbol *core.DigitGroupingSymbol `json:"digitGroupingSymbol" binding:"omitempty,min=0,max=4"`
DigitGrouping *core.DigitGroupingType `json:"digitGrouping" binding:"omitempty,min=0,max=2"`
CurrencyDisplayType *core.CurrencyDisplayType `json:"currencyDisplayType" binding:"omitempty,min=0,max=11"`
DigitGrouping *core.DigitGroupingType `json:"digitGrouping" binding:"omitempty,min=0,max=3"`
CoordinateDisplayType *core.CoordinateDisplayType `json:"coordinateDisplayType" binding:"omitempty,min=0,max=6"`
ExpenseAmountColor *AmountColorType `json:"expenseAmountColor" binding:"omitempty,min=0,max=4"`
IncomeAmountColor *AmountColorType `json:"incomeAmountColor" binding:"omitempty,min=0,max=4"`
@@ -281,15 +290,18 @@ func (u *User) ToUserBasicInfo(avatarProvider core.UserAvatarProviderType, avata
DefaultCurrency: u.DefaultCurrency,
FirstDayOfWeek: u.FirstDayOfWeek,
FiscalYearStart: fiscalYearStart,
CalendarDisplayType: u.CalendarDisplayType,
DateDisplayType: u.DateDisplayType,
LongDateFormat: u.LongDateFormat,
ShortDateFormat: u.ShortDateFormat,
LongTimeFormat: u.LongTimeFormat,
ShortTimeFormat: u.ShortTimeFormat,
DecimalSeparator: u.DecimalSeparator,
FiscalYearFormat: u.FiscalYearFormat,
CurrencyDisplayType: u.CurrencyDisplayType,
NumeralSystem: u.NumeralSystem,
DigitGroupingSymbol: u.DigitGroupingSymbol,
DigitGrouping: u.DigitGrouping,
CurrencyDisplayType: u.CurrencyDisplayType,
CoordinateDisplayType: u.CoordinateDisplayType,
ExpenseAmountColor: u.ExpenseAmountColor,
IncomeAmountColor: u.IncomeAmountColor,
+26 -4
View File
@@ -7,7 +7,7 @@ import (
// RequestIdContainer contains the current request id generator
type RequestIdContainer struct {
Current RequestIdGenerator
current RequestIdGenerator
}
// Initialize a request id container singleton instance
@@ -23,11 +23,33 @@ func InitializeRequestIdGenerator(c core.Context, config *settings.Config) error
return err
}
Container.Current = generator
Container.current = generator
return nil
}
// GenerateRequestId returns a new request id by the current request id generator
func (u *RequestIdContainer) GenerateRequestId(clientIpAddr string, clientPort uint16) string {
return u.Current.GenerateRequestId(clientIpAddr, clientPort)
func (r *RequestIdContainer) GenerateRequestId(clientIpAddr string, clientPort uint16) string {
if r.current == nil {
return ""
}
return r.current.GenerateRequestId(clientIpAddr, clientPort)
}
// GetCurrentServerUniqId returns current server unique id
func (r *RequestIdContainer) GetCurrentServerUniqId() uint16 {
if r.current == nil {
return 0
}
return r.current.GetCurrentServerUniqId()
}
// GetCurrentInstanceUniqId returns current application instance unique id
func (r *RequestIdContainer) GetCurrentInstanceUniqId() uint16 {
if r.current == nil {
return 0
}
return r.current.GetCurrentInstanceUniqId()
}
+63 -1
View File
@@ -1,6 +1,7 @@
package services
import (
"fmt"
"strings"
"time"
@@ -56,6 +57,28 @@ func (s *AccountService) GetAllAccountsByUid(c core.Context, uid int64) ([]*mode
return accounts, err
}
// GetAccountByAccountId returns account model according to account id
func (s *AccountService) GetAccountByAccountId(c core.Context, uid int64, accountId int64) (*models.Account, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if accountId <= 0 {
return nil, errs.ErrAccountIdInvalid
}
account := &models.Account{}
has, err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=? AND account_id=?", uid, false, accountId).Get(account)
if err != nil {
return nil, err
} else if !has {
return nil, errs.ErrAccountNotFound
}
return account, err
}
// GetAccountAndSubAccountsByAccountId returns account model and sub-account models according to account id
func (s *AccountService) GetAccountAndSubAccountsByAccountId(c core.Context, uid int64, accountId int64) ([]*models.Account, error) {
if uid <= 0 {
@@ -640,9 +663,15 @@ func (s *AccountService) DeleteAccount(c core.Context, uid int64, accountId int6
return errs.ErrAccountNotFound
}
var accountAndSubAccountIdsConditions strings.Builder
accountAndSubAccountIds := make([]int64, len(accountAndSubAccounts))
for i := 0; i < len(accountAndSubAccounts); i++ {
if accountAndSubAccountIdsConditions.Len() > 0 {
accountAndSubAccountIdsConditions.WriteString(",")
}
accountAndSubAccountIdsConditions.WriteString("?")
accountAndSubAccountIds[i] = accountAndSubAccounts[i].AccountId
}
@@ -669,6 +698,31 @@ func (s *AccountService) DeleteAccount(c core.Context, uid int64, accountId int6
}
}
transactionTemplateQueryCondition := fmt.Sprintf("uid=? AND deleted=? AND (template_type=? OR (template_type=? AND scheduled_frequency_type<>? AND (scheduled_end_time IS NULL OR scheduled_end_time>=?))) AND (account_id IN (%s) OR related_account_id IN (%s))", accountAndSubAccountIdsConditions.String(), accountAndSubAccountIdsConditions.String())
transactionTemplateQueryConditionParams := make([]any, 0, len(accountAndSubAccountIds)*2+6)
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, uid)
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, false)
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, models.TRANSACTION_TEMPLATE_TYPE_NORMAL)
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE)
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED)
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, now)
for i := 0; i < len(accountAndSubAccountIds); i++ {
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, accountAndSubAccountIds[i])
}
for i := 0; i < len(accountAndSubAccountIds); i++ {
transactionTemplateQueryConditionParams = append(transactionTemplateQueryConditionParams, accountAndSubAccountIds[i])
}
exists, err := sess.Cols("uid", "deleted", "account_id", "related_account_id", "template_type", "scheduled_frequency_type", "scheduled_end_time").Where(transactionTemplateQueryCondition, transactionTemplateQueryConditionParams...).Limit(1).Exist(&models.TransactionTemplate{})
if err != nil {
return err
} else if exists {
return errs.ErrAccountInUseCannotBeDeleted
}
deletedRows, err := sess.Cols("balance", "deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).In("account_id", accountAndSubAccountIds).Update(updateModel)
if err != nil {
@@ -750,6 +804,14 @@ func (s *AccountService) DeleteSubAccount(c core.Context, uid int64, accountId i
}
}
exists, err := sess.Cols("uid", "deleted", "account_id", "related_account_id", "template_type", "scheduled_frequency_type", "scheduled_end_time").Where("uid=? AND deleted=? AND (template_type=? OR (template_type=? AND scheduled_frequency_type<>? AND (scheduled_end_time IS NULL OR scheduled_end_time>=?))) AND (account_id=? OR related_account_id=?)", uid, false, models.TRANSACTION_TEMPLATE_TYPE_NORMAL, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED, now, accountId, accountId).Limit(1).Exist(&models.TransactionTemplate{})
if err != nil {
return err
} else if exists {
return errs.ErrSubAccountInUseCannotBeDeleted
}
deletedRows, err := sess.Cols("balance", "deleted", "deleted_unix_time").Where("uid=? AND deleted=? AND account_id=?", uid, false, accountId).Update(updateModel)
if err != nil {
@@ -827,7 +889,7 @@ func (s *AccountService) GetAccountNames(accounts []*models.Account) []string {
}
// GetAccountOrSubAccountIds returns a list of account ids or sub-account ids according to given account ids
func (s *AccountService) GetAccountOrSubAccountIds(c *core.WebContext, accountIds string, uid int64) ([]int64, error) {
func (s *AccountService) GetAccountOrSubAccountIds(c core.Context, accountIds string, uid int64) ([]int64, error) {
if accountIds == "" || accountIds == "0" {
return nil, nil
}
+19 -23
View File
@@ -4,8 +4,8 @@ import (
"fmt"
"path/filepath"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/mail"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/storage"
@@ -60,7 +60,7 @@ type ServiceUsingConfig struct {
// CurrentConfig returns the current config
func (s *ServiceUsingConfig) CurrentConfig() *settings.Config {
return s.container.Current
return s.container.GetCurrentConfig()
}
// ServiceUsingMailer represents a service that need to use mailer
@@ -70,11 +70,7 @@ type ServiceUsingMailer struct {
// SendMail sends an email according to argument
func (s *ServiceUsingMailer) SendMail(message *mail.MailMessage) error {
if s.container.Current == nil {
return errs.ErrSMTPServerNotEnabled
}
return s.container.Current.SendMail(message)
return s.container.SendMail(message)
}
// ServiceUsingUuid represents a service that need to use uuid
@@ -98,43 +94,43 @@ type ServiceUsingStorage struct {
}
// ExistsAvatar returns whether the user avatar exists from the current avatar object storage
func (s *ServiceUsingStorage) ExistsAvatar(uid int64, fileExtension string) (bool, error) {
return s.container.ExistsAvatar(s.getUserAvatarPath(uid, fileExtension))
func (s *ServiceUsingStorage) ExistsAvatar(ctx core.Context, uid int64, fileExtension string) (bool, error) {
return s.container.ExistsAvatar(ctx, s.getUserAvatarPath(uid, fileExtension))
}
// ReadAvatar returns the user avatar from the current avatar object storage
func (s *ServiceUsingStorage) ReadAvatar(uid int64, fileExtension string) (storage.ObjectInStorage, error) {
return s.container.ReadAvatar(s.getUserAvatarPath(uid, fileExtension))
func (s *ServiceUsingStorage) ReadAvatar(ctx core.Context, uid int64, fileExtension string) (storage.ObjectInStorage, error) {
return s.container.ReadAvatar(ctx, s.getUserAvatarPath(uid, fileExtension))
}
// SaveAvatar returns whether save the user avatar into the current avatar object storage successfully
func (s *ServiceUsingStorage) SaveAvatar(uid int64, object storage.ObjectInStorage, fileExtension string) error {
return s.container.SaveAvatar(s.getUserAvatarPath(uid, fileExtension), object)
func (s *ServiceUsingStorage) SaveAvatar(ctx core.Context, uid int64, object storage.ObjectInStorage, fileExtension string) error {
return s.container.SaveAvatar(ctx, s.getUserAvatarPath(uid, fileExtension), object)
}
// DeleteAvatar returns whether delete the user avatar from the current avatar object storage successfully
func (s *ServiceUsingStorage) DeleteAvatar(uid int64, fileExtension string) error {
return s.container.DeleteAvatar(s.getUserAvatarPath(uid, fileExtension))
func (s *ServiceUsingStorage) DeleteAvatar(ctx core.Context, uid int64, fileExtension string) error {
return s.container.DeleteAvatar(ctx, s.getUserAvatarPath(uid, fileExtension))
}
// ExistsTransactionPicture returns whether the transaction picture exists from the current transaction picture object storage
func (s *ServiceUsingStorage) ExistsTransactionPicture(uid int64, pictureId int64, fileExtension string) (bool, error) {
return s.container.ExistsTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension))
func (s *ServiceUsingStorage) ExistsTransactionPicture(ctx core.Context, uid int64, pictureId int64, fileExtension string) (bool, error) {
return s.container.ExistsTransactionPicture(ctx, s.getTransactionPicturePath(uid, pictureId, fileExtension))
}
// ReadTransactionPicture returns the transaction picture from the current transaction picture object storage
func (s *ServiceUsingStorage) ReadTransactionPicture(uid int64, pictureId int64, fileExtension string) (storage.ObjectInStorage, error) {
return s.container.ReadTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension))
func (s *ServiceUsingStorage) ReadTransactionPicture(ctx core.Context, uid int64, pictureId int64, fileExtension string) (storage.ObjectInStorage, error) {
return s.container.ReadTransactionPicture(ctx, s.getTransactionPicturePath(uid, pictureId, fileExtension))
}
// SaveTransactionPicture returns whether save the transaction picture into the current transaction picture object storage successfully
func (s *ServiceUsingStorage) SaveTransactionPicture(uid int64, pictureId int64, object storage.ObjectInStorage, fileExtension string) error {
return s.container.SaveTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension), object)
func (s *ServiceUsingStorage) SaveTransactionPicture(ctx core.Context, uid int64, pictureId int64, object storage.ObjectInStorage, fileExtension string) error {
return s.container.SaveTransactionPicture(ctx, s.getTransactionPicturePath(uid, pictureId, fileExtension), object)
}
// DeleteTransactionPicture returns whether delete the transaction picture from the current transaction picture object storage successfully
func (s *ServiceUsingStorage) DeleteTransactionPicture(uid int64, pictureId int64, fileExtension string) error {
return s.container.DeleteTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension))
func (s *ServiceUsingStorage) DeleteTransactionPicture(ctx core.Context, uid int64, pictureId int64, fileExtension string) error {
return s.container.DeleteTransactionPicture(ctx, s.getTransactionPicturePath(uid, pictureId, fileExtension))
}
func (s *ServiceUsingStorage) getUserAvatarPath(uid int64, fileExtension string) string {
+16 -26
View File
@@ -1,6 +1,7 @@
package services
import (
"errors"
"fmt"
"math"
"strings"
@@ -71,19 +72,9 @@ func (s *TokenService) GetAllUnexpiredNormalAndMCPTokensByUid(c core.Context, ui
return tokenRecords, err
}
// ParseTokenByHeader returns the token model according to request data
func (s *TokenService) ParseTokenByHeader(c *core.WebContext) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, request.BearerExtractor{})
}
// ParseTokenByArgument returns the token model according to request data
func (s *TokenService) ParseTokenByArgument(c *core.WebContext, tokenParameterName string) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, request.ArgumentExtractor{tokenParameterName})
}
// ParseTokenByCookie returns the token model according to request data
func (s *TokenService) ParseTokenByCookie(c *core.WebContext, tokenCookieName string) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, utils.CookieExtractor{tokenCookieName})
// ParseToken returns the token model according to token content
func (s *TokenService) ParseToken(c core.Context, token string) (*jwt.Token, *core.UserTokenClaims, error) {
return s.parseToken(c, token)
}
// CreateTokenViaCli generates a new normal token and saves to database
@@ -328,53 +319,52 @@ func (s *TokenService) GenerateTokenId(tokenRecord *models.TokenRecord) string {
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
}
func (s *TokenService) parseToken(c *core.WebContext, extractor request.Extractor) (*jwt.Token, *core.UserTokenClaims, error) {
func (s *TokenService) parseToken(c core.Context, tokenString string) (*jwt.Token, *core.UserTokenClaims, error) {
claims := &core.UserTokenClaims{}
token, err := request.ParseFromRequest(c.Request, extractor,
token, err := jwt.ParseWithClaims(tokenString, claims,
func(token *jwt.Token) (any, error) {
now := time.Now().Unix()
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.Warnf(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%d\" is invalid, because %s", claims.UserTokenId, claims.Uid, err.Error())
log.Warnf(c, "[tokens.parseToken] token \"utid:%s\" in token of user \"uid:%d\" is invalid, because %s", claims.UserTokenId, claims.Uid, err.Error())
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(c, claims.Uid, userTokenId, claims.IssuedAt)
if err != nil {
log.Warnf(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%d\" record not found, because %s", claims.UserTokenId, claims.Uid, err.Error())
log.Warnf(c, "[tokens.parseToken] token \"utid:%s\" of user \"uid:%d\" record not found, because %s", claims.UserTokenId, claims.Uid, err.Error())
return nil, errs.ErrTokenRecordNotFound
}
if tokenRecord.ExpiredUnixTime < now {
log.Warnf(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%d\" record is expired", claims.UserTokenId, claims.Uid)
log.Warnf(c, "[tokens.parseToken] token \"utid:%s\" of user \"uid:%d\" record is expired", claims.UserTokenId, claims.Uid)
return nil, errs.ErrTokenExpired
}
return []byte(tokenRecord.Secret), nil
},
request.WithClaims(claims),
request.WithParser(jwt.NewParser(jwt.WithIssuedAt())),
jwt.WithIssuedAt(),
)
if err != nil {
if err == request.ErrNoTokenInRequest {
if errors.Is(err, request.ErrNoTokenInRequest) {
return nil, nil, errs.ErrTokenIsEmpty
}
if err == jwt.ErrTokenMalformed || err == jwt.ErrTokenUnverifiable || err == jwt.ErrTokenSignatureInvalid {
log.Warnf(c, "[tokens.ParseToken] token is invalid, because %s", err.Error())
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) || errors.Is(err, jwt.ErrTokenSignatureInvalid) {
log.Warnf(c, "[tokens.parseToken] token is invalid, because %s", err.Error())
return nil, nil, errs.ErrCurrentInvalidToken
}
if err == jwt.ErrTokenExpired {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, nil, errs.ErrCurrentTokenExpired
}
if err == jwt.ErrTokenUsedBeforeIssued {
log.Warnf(c, "[tokens.ParseToken] token is invalid, because issue time is later than now")
if errors.Is(err, jwt.ErrTokenUsedBeforeIssued) {
log.Warnf(c, "[tokens.parseToken] token is invalid, because issue time is later than now")
return nil, nil, errs.ErrCurrentInvalidToken
}
+9 -1
View File
@@ -397,6 +397,14 @@ func (s *TransactionCategoryService) DeleteCategory(c core.Context, uid int64, c
return errs.ErrTransactionCategoryInUseCannotBeDeleted
}
exists, err = sess.Cols("uid", "deleted", "category_id", "template_type", "scheduled_frequency_type", "scheduled_end_time").Where("uid=? AND deleted=? AND (template_type=? OR (template_type=? AND scheduled_frequency_type<>? AND (scheduled_end_time IS NULL OR scheduled_end_time>=?)))", uid, false, models.TRANSACTION_TEMPLATE_TYPE_NORMAL, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED, now).In("category_id", categoryAndSubCategoryIds).Limit(1).Exist(&models.TransactionTemplate{})
if err != nil {
return err
} else if exists {
return errs.ErrTransactionCategoryInUseCannotBeDeleted
}
deletedRows, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).In("category_id", categoryAndSubCategoryIds).Update(updateModel)
if err != nil {
@@ -543,7 +551,7 @@ func (s *TransactionCategoryService) GetCategoryNames(categories []*models.Trans
}
// GetCategoryOrSubCategoryIds returns all category ids and sub-category ids according to given category ids
func (s *TransactionCategoryService) GetCategoryOrSubCategoryIds(c *core.WebContext, categoryIds string, uid int64) ([]int64, error) {
func (s *TransactionCategoryService) GetCategoryOrSubCategoryIds(c core.Context, categoryIds string, uid int64) ([]int64, error) {
if categoryIds == "" || categoryIds == "0" {
return nil, nil
}
+2 -2
View File
@@ -159,7 +159,7 @@ func (s *TransactionPictureService) GetPictureByPictureId(c core.Context, uid in
return nil, errs.ErrTransactionPictureExtensionInvalid
}
pictureFile, err := s.ReadTransactionPicture(pictureInfo.Uid, pictureInfo.PictureId, pictureInfo.PictureExtension)
pictureFile, err := s.ReadTransactionPicture(c, pictureInfo.Uid, pictureInfo.PictureId, pictureInfo.PictureExtension)
if os.IsNotExist(err) {
return nil, errs.ErrTransactionPictureNoExists
@@ -199,7 +199,7 @@ func (s *TransactionPictureService) UploadPicture(c core.Context, pictureInfo *m
pictureInfo.CreatedUnixTime = time.Now().Unix()
pictureInfo.UpdatedUnixTime = time.Now().Unix()
err := s.SaveTransactionPicture(pictureInfo.Uid, pictureInfo.PictureId, pictureFile, pictureInfo.PictureExtension)
err := s.SaveTransactionPicture(c, pictureInfo.Uid, pictureInfo.PictureId, pictureFile, pictureInfo.PictureExtension)
if err != nil {
return err
+22
View File
@@ -396,6 +396,28 @@ func (s *TransactionTagService) DeleteTag(c core.Context, uid int64, tagId int64
return errs.ErrTransactionTagInUseCannotBeDeleted
}
var relatedTransactionTemplatesByTag []*models.TransactionTemplate
err = sess.Cols("uid", "deleted", "tag_ids", "template_type", "scheduled_frequency_type", "scheduled_end_time").Where("uid=? AND deleted=? AND (template_type=? OR (template_type=? AND scheduled_frequency_type<>? AND (scheduled_end_time IS NULL OR scheduled_end_time>=?))) AND tag_ids LIKE ?", uid, false, models.TRANSACTION_TEMPLATE_TYPE_NORMAL, models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE, models.TRANSACTION_SCHEDULE_FREQUENCY_TYPE_DISABLED, now, "%%"+utils.Int64ToString(tagId)+"%%").Find(&relatedTransactionTemplatesByTag)
if err != nil {
return err
}
for i := 0; i < len(relatedTransactionTemplatesByTag); i++ {
template := relatedTransactionTemplatesByTag[i]
tagIds, err := s.GetTagIds(template.TagIds)
if err != nil {
return err
}
for j := 0; j < len(tagIds); j++ {
if tagIds[j] == tagId {
return errs.ErrTransactionTagInUseCannotBeDeleted
}
}
}
deletedRows, err := sess.ID(tagId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
if err != nil {
+96 -1
View File
@@ -2,6 +2,7 @@ package services
import (
"time"
"xorm.io/xorm"
"github.com/mayswind/ezbookkeeping/pkg/core"
@@ -122,7 +123,13 @@ func (s *TransactionTemplateService) CreateTemplate(c core.Context, template *mo
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
_, err := sess.Insert(template)
err := s.isTemplateValid(sess, template)
if err != nil {
return err
}
_, err = sess.Insert(template)
return err
})
}
@@ -136,6 +143,12 @@ func (s *TransactionTemplateService) ModifyTemplate(c core.Context, template *mo
template.UpdatedUnixTime = time.Now().Unix()
return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error {
err := s.isTemplateValid(sess, template)
if err != nil {
return err
}
updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_start_time", "scheduled_end_time", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template)
if err != nil {
@@ -249,3 +262,85 @@ func (s *TransactionTemplateService) DeleteAllTemplates(c core.Context, uid int6
return nil
})
}
func (s *TransactionTemplateService) isTemplateValid(sess *xorm.Session, template *models.TransactionTemplate) error {
// check accounts are valid
sourceAccount := &models.Account{}
destinationAccount := &models.Account{}
has, err := sess.ID(template.AccountId).Where("uid=? AND deleted=?", template.Uid, false).Get(sourceAccount)
if err != nil {
return err
} else if !has {
return errs.ErrSourceAccountNotFound
}
if sourceAccount.Hidden {
return errs.ErrCannotUseHiddenAccount
}
if template.Type == models.TRANSACTION_TYPE_TRANSFER {
if template.RelatedAccountId <= 0 {
return errs.ErrAccountIdInvalid
} else {
has, err = sess.ID(template.RelatedAccountId).Where("uid=? AND deleted=?", template.Uid, false).Get(destinationAccount)
if err != nil {
return err
} else if !has {
return errs.ErrDestinationAccountNotFound
}
if destinationAccount.Hidden {
return errs.ErrCannotUseHiddenAccount
}
}
}
if sourceAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS || (destinationAccount != nil && destinationAccount.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS) {
return errs.ErrCannotAddTransactionToParentAccount
}
// check category is valid
category := &models.TransactionCategory{}
has, err = sess.ID(template.CategoryId).Where("uid=? AND deleted=?", template.Uid, false).Get(category)
if err != nil {
return err
} else if !has {
return errs.ErrTransactionCategoryNotFound
}
if category.Hidden {
return errs.ErrCannotUseHiddenTransactionCategory
}
if category.ParentCategoryId == models.LevelOneTransactionCategoryParentId {
return errs.ErrCannotUsePrimaryCategoryForTransaction
}
if (template.Type == models.TRANSACTION_TYPE_INCOME && category.Type != models.CATEGORY_TYPE_INCOME) ||
(template.Type == models.TRANSACTION_TYPE_EXPENSE && category.Type != models.CATEGORY_TYPE_EXPENSE) ||
(template.Type == models.TRANSACTION_TYPE_TRANSFER && category.Type != models.CATEGORY_TYPE_TRANSFER) {
return errs.ErrTransactionCategoryTypeInvalid
}
// check tags are valid
tagIds := template.GetTagIds()
var tags []*models.TransactionTag
err = sess.Where("uid=? AND deleted=?", template.Uid, false).In("tag_id", tagIds).Find(&tags)
if err != nil {
return err
} else if len(tags) < len(tagIds) {
return errs.ErrTransactionTagNotFound
}
for i := 0; i < len(tags); i++ {
if tags[i].Hidden {
return errs.ErrCannotUseHiddenTransactionTag
}
}
return nil
}
+97 -7
View File
@@ -107,6 +107,96 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int
return allTransactions, nil
}
// GetAllTransactionsWithAccountBalanceByMaxTime returns account statement within time range
func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) {
if maxTransactionTime <= 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
}
var allTransactions []*models.Transaction
for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCount, false, true)
if err != nil {
return nil, 0, 0, 0, 0, err
}
allTransactions = append(allTransactions, transactions...)
if len(transactions) < int(pageCount) {
maxTransactionTime = 0
break
}
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
}
allTransactionsAndAccountBalance := make([]*models.TransactionWithAccountBalance, 0, len(allTransactions))
if len(allTransactions) < 1 {
return allTransactionsAndAccountBalance, 0, 0, 0, 0, nil
}
totalInflows := int64(0)
totalOutflows := int64(0)
openingBalance := int64(0)
accumulatedBalance := int64(0)
lastAccumulatedBalance := int64(0)
for i := len(allTransactions) - 1; i >= 0; i-- {
transaction := allTransactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
accumulatedBalance = accumulatedBalance + transaction.RelatedAccountAmount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
accumulatedBalance = accumulatedBalance + transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
accumulatedBalance = accumulatedBalance - transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
accumulatedBalance = accumulatedBalance - transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
accumulatedBalance = accumulatedBalance + transaction.Amount
} else {
log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
return nil, 0, 0, 0, 0, errs.ErrTransactionTypeInvalid
}
if transaction.TransactionTime < minTransactionTime {
openingBalance = accumulatedBalance
lastAccumulatedBalance = accumulatedBalance
continue
}
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if accountCategory.IsAsset() {
totalInflows = totalInflows + transaction.RelatedAccountAmount
} else if accountCategory.IsLiability() {
totalOutflows = totalOutflows - transaction.RelatedAccountAmount
}
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
totalInflows = totalInflows + transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
totalOutflows = totalOutflows + transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
totalOutflows = totalOutflows + transaction.Amount
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
totalInflows = totalInflows + transaction.Amount
}
transactionsAndAccountBalance := &models.TransactionWithAccountBalance{
Transaction: transaction,
AccountOpeningBalance: lastAccumulatedBalance,
AccountClosingBalance: accumulatedBalance,
}
lastAccumulatedBalance = accumulatedBalance
allTransactionsAndAccountBalance = append(allTransactionsAndAccountBalance, transactionsAndAccountBalance)
}
return allTransactionsAndAccountBalance, totalInflows, totalOutflows, openingBalance, accumulatedBalance, nil
}
// GetTransactionsByMaxTime returns transactions before given time
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {
@@ -580,7 +670,7 @@ func (s *TransactionService) CreateScheduledTransactions(c core.Context, current
log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" has created a new trasaction \"id:%d\"", template.TemplateId, transaction.TransactionId)
} else {
failedCount++
log.Errorf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" failed to create new trasaction", template.TemplateId)
log.Errorf(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" failed to create new trasaction, because %s", template.TemplateId, err.Error())
}
}
@@ -912,7 +1002,7 @@ func (s *TransactionService) ModifyTransaction(c core.Context, transaction *mode
return errs.ErrBalanceModificationTransactionCannotChangeAccountId
}
if transaction.RelatedAccountAmount != oldTransaction.RelatedAccountAmount {
if transaction.Amount != oldTransaction.Amount && transaction.RelatedAccountAmount != oldTransaction.RelatedAccountAmount {
sourceAccount.UpdatedUnixTime = time.Now().Unix()
updatedRows, err := sess.ID(sourceAccount.AccountId).SetExpr("balance", fmt.Sprintf("balance-(%d)+(%d)", oldTransaction.RelatedAccountAmount, transaction.RelatedAccountAmount)).Cols("updated_unix_time").Where("uid=? AND deleted=?", sourceAccount.Uid, false).Update(sourceAccount)
@@ -1230,7 +1320,7 @@ func (s *TransactionService) DeleteTransaction(c core.Context, uid int64, transa
}
// DeleteAllTransactions deletes all existed transactions from database
func (s *TransactionService) DeleteAllTransactions(c core.Context, uid int64) error {
func (s *TransactionService) DeleteAllTransactions(c core.Context, uid int64, deleteAccount bool) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -1254,12 +1344,12 @@ func (s *TransactionService) DeleteAllTransactions(c core.Context, uid int64) er
accountUpdateModel := &models.Account{
Balance: 0,
Deleted: true,
Deleted: deleteAccount,
DeletedUnixTime: now,
}
return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error {
// Update all transaction to deleted
// Update all transactions to deleted
_, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel)
if err != nil {
@@ -1273,14 +1363,14 @@ func (s *TransactionService) DeleteAllTransactions(c core.Context, uid int64) er
return err
}
// Update all transaction picture to deleted
// Update all transaction pictures to deleted
_, err = sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(pictureUpdateModel)
if err != nil {
return err
}
// Update all account table to deleted
// Update all accounts to deleted or set amount to zero
_, err = sess.Cols("balance", "deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(accountUpdateModel)
if err != nil {
+11 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/datastore"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
@@ -45,7 +46,7 @@ func (s *UserApplicationCloudSettingsService) GetUserApplicationCloudSettingsByU
}
// UpdateUserApplicationCloudSettings updates user application cloud settings
func (s *UserApplicationCloudSettingsService) UpdateUserApplicationCloudSettings(c core.Context, uid int64, settings models.ApplicationCloudSettingSlice) error {
func (s *UserApplicationCloudSettingsService) UpdateUserApplicationCloudSettings(c core.Context, uid int64, settings models.ApplicationCloudSettingSlice, forceUpdate bool, lastUpdateTime int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
@@ -65,14 +66,21 @@ func (s *UserApplicationCloudSettingsService) UpdateUserApplicationCloudSettings
return err
}
updatedRows := int64(0)
if !exists {
_, err = sess.Insert(userApplicationCloudSetting)
updatedRows, err = sess.Insert(userApplicationCloudSetting)
} else if forceUpdate || lastUpdateTime <= 0 {
updatedRows, err = sess.ID(uid).Cols("settings", "updated_unix_time").Update(userApplicationCloudSetting)
} else {
_, err = sess.ID(uid).Cols("settings", "updated_unix_time").Update(userApplicationCloudSetting)
updatedRows, err = sess.ID(uid).Cols("settings", "updated_unix_time").Where("updated_unix_time=?", lastUpdateTime).Update(userApplicationCloudSetting)
}
if err != nil {
return err
} else if updatedRows < 1 {
log.Errorf(c, "[user_app_cloud_settings.UpdateUserApplicationCloudSettings] failed to update user application cloud settings")
return errs.ErrDatabaseOperationFailed
}
return nil
+21 -9
View File
@@ -162,7 +162,7 @@ func (s *UserService) GetUserAvatar(c core.Context, uid int64, fileExtension str
return nil, errs.ErrUserAvatarExtensionInvalid
}
avatarFile, err := s.ReadAvatar(user.Uid, user.CustomAvatarType)
avatarFile, err := s.ReadAvatar(c, user.Uid, user.CustomAvatarType)
if os.IsNotExist(err) {
return nil, errs.ErrUserAvatarNoExists
@@ -293,6 +293,14 @@ func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLa
updateCols = append(updateCols, "fiscal_year_start")
}
if core.CALENDAR_DISPLAY_TYPE_DEFAULT <= user.CalendarDisplayType && user.CalendarDisplayType <= core.CALENDAR_DISPLAY_TYPE_BUDDHIST {
updateCols = append(updateCols, "calendar_display_type")
}
if core.DATE_DISPLAY_TYPE_DEFAULT <= user.DateDisplayType && user.DateDisplayType <= core.DATE_DISPLAY_TYPE_BUDDHIST {
updateCols = append(updateCols, "date_display_type")
}
if core.LONG_DATE_FORMAT_DEFAULT <= user.LongDateFormat && user.LongDateFormat <= core.LONG_DATE_FORMAT_D_M_YYYY {
updateCols = append(updateCols, "long_date_format")
}
@@ -313,6 +321,14 @@ func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLa
updateCols = append(updateCols, "fiscal_year_format")
}
if core.CURRENCY_DISPLAY_TYPE_DEFAULT <= user.CurrencyDisplayType && user.CurrencyDisplayType <= core.CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT {
updateCols = append(updateCols, "currency_display_type")
}
if core.NUMERAL_SYSTEM_DEFAULT <= user.NumeralSystem && user.NumeralSystem <= core.NUMERAL_SYSTEM_DEVANAGARI_NUMERALS {
updateCols = append(updateCols, "numeral_system")
}
if core.DECIMAL_SEPARATOR_DEFAULT <= user.DecimalSeparator && user.DecimalSeparator <= core.DECIMAL_SEPARATOR_COMMA {
updateCols = append(updateCols, "decimal_separator")
}
@@ -321,14 +337,10 @@ func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLa
updateCols = append(updateCols, "digit_grouping_symbol")
}
if core.DIGIT_GROUPING_TYPE_DEFAULT <= user.DigitGrouping && user.DigitGrouping <= core.DIGIT_GROUPING_TYPE_THOUSANDS_SEPARATOR {
if core.DIGIT_GROUPING_TYPE_DEFAULT <= user.DigitGrouping && user.DigitGrouping <= core.DIGIT_GROUPING_TYPE_INDIAN_NUMBER_GROUPING {
updateCols = append(updateCols, "digit_grouping")
}
if core.CURRENCY_DISPLAY_TYPE_DEFAULT <= user.CurrencyDisplayType && user.CurrencyDisplayType <= core.CURRENCY_DISPLAY_TYPE_NAME_AFTER_AMOUNT {
updateCols = append(updateCols, "currency_display_type")
}
if core.COORDINATE_DISPLAY_TYPE_DEFAULT <= user.CoordinateDisplayType && user.CoordinateDisplayType <= core.COORDINATE_DISPLAY_TYPE_LONGITUDE_LATITUDE_DEGREES_MINUTES_SECONDS {
updateCols = append(updateCols, "coordinate_display_type")
}
@@ -371,7 +383,7 @@ func (s *UserService) UpdateUserAvatar(c core.Context, uid int64, avatarFile mul
defer avatarFile.Close()
err := s.SaveAvatar(uid, avatarFile, fileExtension)
err := s.SaveAvatar(c, uid, avatarFile, fileExtension)
if err != nil {
return err
@@ -394,7 +406,7 @@ func (s *UserService) UpdateUserAvatar(c core.Context, uid int64, avatarFile mul
}
if fileExtension != oldFileExtension && oldFileExtension != "" {
err = s.DeleteAvatar(uid, oldFileExtension)
err = s.DeleteAvatar(c, uid, oldFileExtension)
if err != nil {
log.Warnf(c, "[users.UpdateUserAvatar] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", oldFileExtension, uid, err.Error())
@@ -410,7 +422,7 @@ func (s *UserService) RemoveUserAvatar(c core.Context, uid int64, fileExtension
return errs.ErrUserIdInvalid
}
err := s.DeleteAvatar(uid, fileExtension)
err := s.DeleteAvatar(c, uid, fileExtension)
if err != nil && !os.IsNotExist(err) {
return err
+27 -1
View File
@@ -63,6 +63,7 @@ const (
const (
LocalFileSystemObjectStorageType string = "local_filesystem"
MinIOStorageType string = "minio"
WebDAVStorageType string = "webdav"
)
// Uuid generator types
@@ -137,6 +138,8 @@ const (
defaultLogFileMaxSize uint32 = 104857600 // 100 MB
defaultLogFileMaxDays uint32 = 7 // days
defaultWebDAVRequestTimeout uint32 = 10000 // 10 seconds
defaultInMemoryDuplicateCheckerCleanupInterval uint32 = 60 // 1 minutes
defaultDuplicateSubmissionsInterval uint32 = 300 // 5 minutes
@@ -195,6 +198,17 @@ type MinIOConfig struct {
RootPath string
}
// WebDAVConfig represents the WebDAV setting config
type WebDAVConfig struct {
Url string
Username string
Password string
RootPath string
RequestTimeout uint32
Proxy string
SkipTLSVerify bool
}
// TipConfig represents a tip setting config
type TipConfig struct {
Enabled bool
@@ -264,6 +278,7 @@ type Config struct {
StorageType string
LocalFileSystemPath string
MinIOConfig *MinIOConfig
WebDAVConfig *WebDAVConfig
// Uuid
UuidGeneratorType string
@@ -697,6 +712,8 @@ func loadStorageConfiguration(config *Config, configFile *ini.File, sectionName
config.StorageType = LocalFileSystemObjectStorageType
} else if getConfigItemStringValue(configFile, sectionName, "type") == MinIOStorageType {
config.StorageType = MinIOStorageType
} else if getConfigItemStringValue(configFile, sectionName, "type") == WebDAVStorageType {
config.StorageType = WebDAVStorageType
} else {
return errs.ErrInvalidStorageType
}
@@ -718,9 +735,18 @@ func loadStorageConfiguration(config *Config, configFile *ini.File, sectionName
minIOConfig.SkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "minio_skip_tls_verify", false)
minIOConfig.Bucket = getConfigItemStringValue(configFile, sectionName, "minio_bucket")
minIOConfig.RootPath = getConfigItemStringValue(configFile, sectionName, "minio_root_path")
config.MinIOConfig = minIOConfig
webDAVConfig := &WebDAVConfig{}
webDAVConfig.Url = getConfigItemStringValue(configFile, sectionName, "webdav_url")
webDAVConfig.Username = getConfigItemStringValue(configFile, sectionName, "webdav_username")
webDAVConfig.Password = getConfigItemStringValue(configFile, sectionName, "webdav_password")
webDAVConfig.RootPath = getConfigItemStringValue(configFile, sectionName, "webdav_root_path")
webDAVConfig.RequestTimeout = getConfigItemUint32Value(configFile, sectionName, "webdav_request_timeout", defaultWebDAVRequestTimeout)
webDAVConfig.Proxy = getConfigItemStringValue(configFile, sectionName, "webdav_proxy", "system")
webDAVConfig.SkipTLSVerify = getConfigItemBoolValue(configFile, sectionName, "webdav_skip_tls_verify", false)
config.WebDAVConfig = webDAVConfig
return nil
}
+7 -2
View File
@@ -2,7 +2,7 @@ package settings
// ConfigContainer contains the current setting config
type ConfigContainer struct {
Current *Config
current *Config
}
// Initialize a config container singleton instance
@@ -15,5 +15,10 @@ var (
// SetCurrentConfig sets the current config by a given config
func SetCurrentConfig(config *Config) {
Container.Current = config
Container.current = config
}
// GetCurrentConfig returns the current config
func (c *ConfigContainer) GetCurrentConfig() *Config {
return c.current
}
+22
View File
@@ -0,0 +1,22 @@
package storage
import (
"bytes"
)
// bytesSliceObject represents a byte slice object in storage
type bytesSliceObject struct {
*bytes.Reader
}
// Close does nothing because it does not hold any resources that need to be released
func (b *bytesSliceObject) Close() error {
return nil
}
// newByteSliceObject creates a new byte slice object from the specified byte slice
func newByteSliceObject(data []byte) ObjectInStorage {
return &bytesSliceObject{
Reader: bytes.NewReader(data),
}
}
+5 -4
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
@@ -28,17 +29,17 @@ func NewLocalFileSystemObjectStorage(config *settings.Config, pathPrefix string)
}
// Exists returns whether the file exists
func (s *LocalFileSystemObjectStorage) Exists(path string) (bool, error) {
func (s *LocalFileSystemObjectStorage) Exists(ctx core.Context, path string) (bool, error) {
return utils.IsExists(s.getFinalPath(path))
}
// Read returns the object instance according to specified the file path
func (s *LocalFileSystemObjectStorage) Read(path string) (ObjectInStorage, error) {
func (s *LocalFileSystemObjectStorage) Read(ctx core.Context, path string) (ObjectInStorage, error) {
return os.Open(s.getFinalPath(path))
}
// Save returns whether save the object instance successfully
func (s *LocalFileSystemObjectStorage) Save(path string, object ObjectInStorage) error {
func (s *LocalFileSystemObjectStorage) Save(ctx core.Context, path string, object ObjectInStorage) error {
finalPath := s.getFinalPath(path)
if err := os.MkdirAll(filepath.Dir(finalPath), os.ModePerm); err != nil {
@@ -59,7 +60,7 @@ func (s *LocalFileSystemObjectStorage) Save(path string, object ObjectInStorage)
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *LocalFileSystemObjectStorage) Delete(path string) error {
func (s *LocalFileSystemObjectStorage) Delete(ctx core.Context, path string) error {
return os.Remove(s.getFinalPath(path))
}
+5 -8
View File
@@ -9,6 +9,7 @@ import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
@@ -64,8 +65,7 @@ func NewMinIOObjectStorage(config *settings.Config, pathPrefix string) (*MinIOOb
}
// Exists returns whether the file exists
func (s *MinIOObjectStorage) Exists(path string) (bool, error) {
ctx := context.Background()
func (s *MinIOObjectStorage) Exists(ctx core.Context, path string) (bool, error) {
objectInfo, err := s.minIOClient.StatObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.StatObjectOptions{})
if err == nil && !objectInfo.IsDeleteMarker {
@@ -76,22 +76,19 @@ func (s *MinIOObjectStorage) Exists(path string) (bool, error) {
}
// Read returns the object instance according to specified the file path
func (s *MinIOObjectStorage) Read(path string) (ObjectInStorage, error) {
ctx := context.Background()
func (s *MinIOObjectStorage) Read(ctx core.Context, path string) (ObjectInStorage, error) {
return s.minIOClient.GetObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.GetObjectOptions{})
}
// Save returns whether save the object instance successfully
func (s *MinIOObjectStorage) Save(path string, object ObjectInStorage) error {
ctx := context.Background()
func (s *MinIOObjectStorage) Save(ctx core.Context, path string, object ObjectInStorage) error {
_, err := s.minIOClient.PutObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), object, -1, minio.PutObjectOptions{})
return err
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *MinIOObjectStorage) Delete(path string) error {
ctx := context.Background()
func (s *MinIOObjectStorage) Delete(ctx core.Context, path string) error {
return s.minIOClient.RemoveObject(ctx, s.minIOConfig.Bucket, s.getFinalPath(path), minio.RemoveObjectOptions{})
}
+6 -4
View File
@@ -1,9 +1,11 @@
package storage
import "github.com/mayswind/ezbookkeeping/pkg/core"
// ObjectStorage represents an object storage to store file object
type ObjectStorage interface {
Exists(path string) (bool, error)
Read(path string) (ObjectInStorage, error)
Save(path string, object ObjectInStorage) error
Delete(path string) error
Exists(ctx core.Context, path string) (bool, error)
Read(ctx core.Context, path string) (ObjectInStorage, error)
Save(ctx core.Context, path string, object ObjectInStorage) error
Delete(ctx core.Context, path string) error
}
+66 -27
View File
@@ -1,6 +1,7 @@
package storage
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
@@ -10,8 +11,8 @@ const transactionPicturePathPrefix = "transaction"
// StorageContainer contains the current object storage
type StorageContainer struct {
AvatarCurrentStorage ObjectStorage
TransactionPictureCurrentStorage ObjectStorage
avatarCurrentStorage ObjectStorage
transactionPictureCurrentStorage ObjectStorage
}
// Initialize a object storage container singleton instance
@@ -21,63 +22,99 @@ var (
// InitializeStorageContainer initializes the current object storage according to the config
func InitializeStorageContainer(config *settings.Config) error {
avatarStorage, err := newObjectStorage(config, avatarPathPrefix)
if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL {
avatarStorage, err := newObjectStorage(config, avatarPathPrefix)
if err != nil {
return err
if err != nil {
return err
}
Container.avatarCurrentStorage = avatarStorage
}
Container.AvatarCurrentStorage = avatarStorage
if config.EnableTransactionPictures {
transactionPictureStorage, err := newObjectStorage(config, transactionPicturePathPrefix)
transactionPictureStorage, err := newObjectStorage(config, transactionPicturePathPrefix)
if err != nil {
return err
}
if err != nil {
return err
Container.transactionPictureCurrentStorage = transactionPictureStorage
}
Container.TransactionPictureCurrentStorage = transactionPictureStorage
return nil
}
// ExistsAvatar returns whether the avatar file exists from the current avatar object storage
func (s *StorageContainer) ExistsAvatar(path string) (bool, error) {
return s.AvatarCurrentStorage.Exists(path)
func (s *StorageContainer) ExistsAvatar(ctx core.Context, path string) (bool, error) {
if s.avatarCurrentStorage == nil {
return false, errs.ErrSystemError
}
return s.avatarCurrentStorage.Exists(ctx, path)
}
// ReadAvatar returns the avatar file from the current avatar object storage
func (s *StorageContainer) ReadAvatar(path string) (ObjectInStorage, error) {
return s.AvatarCurrentStorage.Read(path)
func (s *StorageContainer) ReadAvatar(ctx core.Context, path string) (ObjectInStorage, error) {
if s.avatarCurrentStorage == nil {
return nil, errs.ErrSystemError
}
return s.avatarCurrentStorage.Read(ctx, path)
}
// SaveAvatar returns whether save the avatar file into the current avatar object storage successfully
func (s *StorageContainer) SaveAvatar(path string, object ObjectInStorage) error {
return s.AvatarCurrentStorage.Save(path, object)
func (s *StorageContainer) SaveAvatar(ctx core.Context, path string, object ObjectInStorage) error {
if s.avatarCurrentStorage == nil {
return errs.ErrSystemError
}
return s.avatarCurrentStorage.Save(ctx, path, object)
}
// DeleteAvatar returns whether delete the avatar file from the current avatar object storage successfully
func (s *StorageContainer) DeleteAvatar(path string) error {
return s.AvatarCurrentStorage.Delete(path)
func (s *StorageContainer) DeleteAvatar(ctx core.Context, path string) error {
if s.avatarCurrentStorage == nil {
return errs.ErrSystemError
}
return s.avatarCurrentStorage.Delete(ctx, path)
}
// ExistsTransactionPicture returns whether the transaction picture file exists from the current transaction picture object storage
func (s *StorageContainer) ExistsTransactionPicture(path string) (bool, error) {
return s.TransactionPictureCurrentStorage.Exists(path)
func (s *StorageContainer) ExistsTransactionPicture(ctx core.Context, path string) (bool, error) {
if s.transactionPictureCurrentStorage == nil {
return false, errs.ErrSystemError
}
return s.transactionPictureCurrentStorage.Exists(ctx, path)
}
// ReadTransactionPicture returns the transaction picture file from the current transaction picture object storage
func (s *StorageContainer) ReadTransactionPicture(path string) (ObjectInStorage, error) {
return s.TransactionPictureCurrentStorage.Read(path)
func (s *StorageContainer) ReadTransactionPicture(ctx core.Context, path string) (ObjectInStorage, error) {
if s.transactionPictureCurrentStorage == nil {
return nil, errs.ErrSystemError
}
return s.transactionPictureCurrentStorage.Read(ctx, path)
}
// SaveTransactionPicture returns whether save the transaction picture file into the current transaction picture object storage successfully
func (s *StorageContainer) SaveTransactionPicture(path string, object ObjectInStorage) error {
return s.TransactionPictureCurrentStorage.Save(path, object)
func (s *StorageContainer) SaveTransactionPicture(ctx core.Context, path string, object ObjectInStorage) error {
if s.transactionPictureCurrentStorage == nil {
return errs.ErrSystemError
}
return s.transactionPictureCurrentStorage.Save(ctx, path, object)
}
// DeleteTransactionPicture returns whether delete the transaction picture file from the current transaction picture object storage successfully
func (s *StorageContainer) DeleteTransactionPicture(path string) error {
return s.TransactionPictureCurrentStorage.Delete(path)
func (s *StorageContainer) DeleteTransactionPicture(ctx core.Context, path string) error {
if s.transactionPictureCurrentStorage == nil {
return errs.ErrSystemError
}
return s.transactionPictureCurrentStorage.Delete(ctx, path)
}
func newObjectStorage(config *settings.Config, pathPrefix string) (ObjectStorage, error) {
@@ -85,6 +122,8 @@ func newObjectStorage(config *settings.Config, pathPrefix string) (ObjectStorage
return NewLocalFileSystemObjectStorage(config, pathPrefix)
} else if config.StorageType == settings.MinIOStorageType {
return NewMinIOObjectStorage(config, pathPrefix)
} else if config.StorageType == settings.WebDAVStorageType {
return NewWebDAVObjectStorage(config, pathPrefix)
}
return nil, errs.ErrInvalidStorageType
+360
View File
@@ -0,0 +1,360 @@
package storage
import (
"bytes"
"crypto/tls"
"io"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// WebDAVObjectStorage represents WebDAV object storage
type WebDAVObjectStorage struct {
httpClient *http.Client
webDavConfig *settings.WebDAVConfig
rootPath string
}
// NewWebDAVObjectStorage returns a WebDAV object storage
func NewWebDAVObjectStorage(config *settings.Config, pathPrefix string) (*WebDAVObjectStorage, error) {
webDavConfig := config.WebDAVConfig
transport := http.DefaultTransport.(*http.Transport).Clone()
utils.SetProxyUrl(transport, webDavConfig.Proxy)
if webDavConfig.SkipTLSVerify {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
client := &http.Client{
Transport: transport,
Timeout: time.Duration(webDavConfig.RequestTimeout) * time.Millisecond,
}
storage := &WebDAVObjectStorage{
httpClient: client,
webDavConfig: webDavConfig,
rootPath: webDavConfig.RootPath,
}
storage.rootPath = storage.getFinalPath(pathPrefix)
storage.rootPath = strings.ReplaceAll(storage.rootPath, "\\", "/")
ctx := core.NewNullContext()
exists, err := storage.directoryExists(ctx, storage.rootPath)
if err != nil {
return nil, err
}
if !exists {
err := storage.createAllDirectories(ctx, "", storage.rootPath)
if err != nil {
return nil, err
}
}
return storage, nil
}
// Exists returns whether the file exists
func (s *WebDAVObjectStorage) Exists(ctx core.Context, path string) (bool, error) {
req, err := http.NewRequest("HEAD", s.getFinalFileUrl(path), nil)
if err != nil {
return false, err
}
req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password)
resp, err := s.httpClient.Do(req)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Exists] cannot check file exists, because %s", err.Error())
return false, err
}
if resp.StatusCode == http.StatusOK {
return true, nil
} else if resp.StatusCode == http.StatusNotFound {
return false, nil
}
log.Errorf(ctx, "[webdav_storage.Exists] cannot check file exists, http status code is %d", resp.StatusCode)
return false, errs.ErrSystemError
}
// Read returns the object instance according to specified the file path
func (s *WebDAVObjectStorage) Read(ctx core.Context, path string) (ObjectInStorage, error) {
req, err := http.NewRequest("GET", s.getFinalFileUrl(path), nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password)
resp, err := s.httpClient.Do(req)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Read] cannot get file, because %s", err.Error())
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Read] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error())
return nil, err
}
if resp.StatusCode != http.StatusOK {
log.Errorf(ctx, "[webdav_storage.Read] cannot get file, http status code is %d, response is %s", resp.StatusCode, string(body))
return nil, errs.ErrSystemError
}
return newByteSliceObject(body), nil
}
// Save returns whether save the object instance successfully
func (s *WebDAVObjectStorage) Save(ctx core.Context, path string, object ObjectInStorage) error {
finalPath := s.getFinalPath(path)
dir := strings.ReplaceAll(filepath.Dir(finalPath), "\\", "/")
exists, err := s.directoryExists(ctx, dir)
if err != nil {
return err
}
if !exists {
rootExists, err := s.directoryExists(ctx, s.rootPath)
if err != nil {
return err
}
if !rootExists {
err := s.createAllDirectories(ctx, "", s.rootPath)
if err != nil {
return err
}
}
err = s.createAllDirectories(ctx, s.rootPath, strings.ReplaceAll(filepath.Dir(path), "\\", "/"))
if err != nil {
return err
}
}
data, err := io.ReadAll(object)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", s.getFinalFileUrl(path), bytes.NewReader(data))
if err != nil {
return err
}
req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password)
resp, err := s.httpClient.Do(req)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Save] cannot save file, because %s", err.Error())
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Save] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error())
return err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
log.Errorf(ctx, "[webdav_storage.Save] cannot save file, http status code is %d, response is %s", resp.StatusCode, string(body))
return errs.ErrSystemError
}
return nil
}
// Delete returns whether delete the object according to specified the file path successfully
func (s *WebDAVObjectStorage) Delete(ctx core.Context, path string) error {
req, err := http.NewRequest("DELETE", s.getFinalFileUrl(path), nil)
if err != nil {
return err
}
req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password)
resp, err := s.httpClient.Do(req)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Delete] cannot delete file, because %s", err.Error())
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf(ctx, "[webdav_storage.Delete] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error())
return err
}
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
log.Errorf(ctx, "[webdav_storage.Delete] cannot delete file, http status code is %d, response is %s", resp.StatusCode, string(body))
return errs.ErrSystemError
}
return nil
}
func (s *WebDAVObjectStorage) directoryExists(ctx core.Context, path string) (bool, error) {
req, err := http.NewRequest("PROPFIND", s.getFinalDirectoryUrl(path), nil)
if err != nil {
return false, err
}
req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password)
resp, err := s.httpClient.Do(req)
if err != nil {
log.Errorf(ctx, "[webdav_storage.directoryExists] cannot check directory exists, because %s", err.Error())
return false, err
}
if resp.StatusCode == http.StatusMultiStatus || resp.StatusCode == http.StatusOK {
return true, nil
} else if resp.StatusCode == http.StatusNotFound {
return false, nil
}
log.Errorf(ctx, "[webdav_storage.directoryExists] cannot check directory exists, http status code is %d", resp.StatusCode)
return false, errs.ErrSystemError
}
func (s *WebDAVObjectStorage) createDirectory(ctx core.Context, path string) error {
req, err := http.NewRequest("MKCOL", s.getFinalDirectoryUrl(path), nil)
if err != nil {
return err
}
req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password)
resp, err := s.httpClient.Do(req)
if err != nil {
log.Errorf(ctx, "[webdav_storage.createDirectory] cannot create directory, because %s", err.Error())
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf(ctx, "[webdav_storage.createDirectory] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error())
return err
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusMethodNotAllowed {
log.Errorf(ctx, "[webdav_storage.createDirectory] cannot create directory, http status code is %d, response is %s", resp.StatusCode, string(body))
return errs.ErrSystemError
}
return nil
}
func (s *WebDAVObjectStorage) createAllDirectories(ctx core.Context, currentPath string, path string) error {
directories := strings.Split(path, "/")
for _, dir := range directories {
if len(dir) == 0 {
continue
}
currentPath = currentPath + "/" + dir
exists, err := s.directoryExists(ctx, currentPath)
if err != nil {
return err
}
if !exists {
err = s.createDirectory(ctx, currentPath)
if err != nil {
return err
}
}
}
return nil
}
func (s *WebDAVObjectStorage) getFinalFileUrl(filePath string) string {
finalUrl := s.webDavConfig.Url
if len(finalUrl) < 1 || finalUrl[len(finalUrl)-1] != '/' {
finalUrl = finalUrl + "/"
}
finalPath := s.getFinalPath(filePath)
if len(finalPath) > 0 && finalPath[0] == '/' {
finalPath = finalPath[1:]
}
return finalUrl + finalPath
}
func (s *WebDAVObjectStorage) getFinalDirectoryUrl(dirPath string) string {
finalUrl := s.webDavConfig.Url
if len(finalUrl) < 1 || finalUrl[len(finalUrl)-1] != '/' {
finalUrl = finalUrl + "/"
}
if len(dirPath) > 0 && dirPath[0] == '/' {
dirPath = dirPath[1:]
}
if len(dirPath) > 0 && dirPath[len(dirPath)-1] != '/' {
dirPath = dirPath + "/"
}
return finalUrl + dirPath
}
func (s *WebDAVObjectStorage) getFinalPath(path string) string {
rootPath := s.rootPath
if len(rootPath) < 1 || rootPath[len(rootPath)-1] != '/' {
rootPath = rootPath + "/"
}
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
path = strings.ReplaceAll(path, "\\", "/")
return rootPath + path
}
-20
View File
@@ -1,20 +0,0 @@
package utils
import (
"net/http"
"github.com/golang-jwt/jwt/v5/request"
)
// CookieExtractor extracts a token from request cookies
type CookieExtractor []string
func (e CookieExtractor) ExtractToken(req *http.Request) (string, error) {
for _, arg := range e {
if cookie, _ := req.Cookie(arg); cookie != nil {
return cookie.Value, nil
}
}
return "", request.ErrNoTokenInRequest
}
+12 -4
View File
@@ -7,7 +7,7 @@ import (
// UuidContainer contains the current uuid generator
type UuidContainer struct {
Current UuidGenerator
current UuidGenerator
}
// Initialize a uuid container singleton instance
@@ -19,7 +19,7 @@ var (
func InitializeUuidGenerator(config *settings.Config) error {
if config.UuidGeneratorType == settings.InternalUuidGeneratorType {
generator, err := NewInternalUuidGenerator(config)
Container.Current = generator
Container.current = generator
return err
}
@@ -29,10 +29,18 @@ func InitializeUuidGenerator(config *settings.Config) error {
// GenerateUuid returns a new uuid by the current uuid generator
func (u *UuidContainer) GenerateUuid(uuidType UuidType) int64 {
return u.Current.GenerateUuid(uuidType)
if u.current == nil {
return 0
}
return u.current.GenerateUuid(uuidType)
}
// GenerateUuids returns new uuids by the current uuid generator
func (u *UuidContainer) GenerateUuids(uuidType UuidType, count uint16) []int64 {
return u.Current.GenerateUuids(uuidType, count)
if u.current == nil {
return nil
}
return u.current.GenerateUuids(uuidType, count)
}
+6 -3
View File
@@ -1,5 +1,8 @@
module.exports = {
plugins: {
'postcss-preset-env': {},
},
plugins: {
'autoprefixer': {
logical: false
},
'postcss-preset-env': {},
},
};
+5 -7
View File
@@ -1,8 +1,6 @@
<?xml version="1.0"?>
<svg width="750" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<path d="m2.38122,215.61524l748.61878,-113.11523l0,197.99999l-750,0l1.38122,-84.88476z" fill="#111111" id="svg_3" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0" stroke-width="null"/>
<g id="BACKGROUND"/>
</g>
<?xml version="1.0" encoding="utf-8"?>
<svg width="750" height="300" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<path d="m2.38122,215.61524l748.61878,-113.11523l0,197.99999l-750,0l1.38122,-84.88476z" fill="#272829" id="svg_3" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0" stroke-width="null"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 383 B

+69 -70
View File
@@ -1,73 +1,72 @@
<?xml version="1.0"?>
<svg width="750" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<?xml version="1.0" encoding="utf-8"?>
<svg width="750" height="300" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<path d="m2.381218,216.615238l748.618787,-113.11523l0,197.999993l-750.000002,0l1.381215,-84.884763z" fill="#f9f4ea" id="svg_3" stroke="#000000" stroke-dasharray="null" stroke-linecap="null" stroke-linejoin="null" stroke-opacity="0" stroke-width="null"/>
<g id="BACKGROUND"/>
<g id="svg_1">
<path d="m623,300.642881c-63.833,0 -127.667,0 -191.5,0c0.109,-1.4 0.038,-2.844 0.365,-4.191c0.775,-3.196 1.715,-6.353 2.636,-9.511c0.602,-2.062 1.084,-4.195 1.979,-6.129c2.321,-5.011 3.543,-10.485 6.466,-15.235c0.126,-0.205 0.181,-0.471 0.207,-0.716c0.161,-1.56 1.197,-2.67 1.938,-3.947c1.587,-2.735 3.482,-5.229 5.48,-7.668c1.135,-1.386 2.09,-2.918 3.127,-4.385c0.878,0.766 1.792,1.494 2.627,2.303c3.598,3.486 7.44,6.677 11.787,9.17c6.411,3.678 13.171,5.353 20.6,3.493c4.237,-1.061 8.191,-2.676 12.041,-4.69c7.177,-3.755 13.324,-8.74 18.465,-14.989c0.938,-1.14 1.95,-2.218 2.932,-3.33c1.704,0.772 3.2,1.503 4.737,2.135c5.842,2.405 11.935,3.647 18.246,3.4c5.454,-0.214 10.695,-1.651 15.701,-3.762c10.855,-4.578 21.031,-10.344 30.219,-17.776c0.768,-0.622 1.628,-1.13 2.446,-1.692c2.325,-0.514 4.693,-0.884 6.966,-1.569c6.536,-1.969 12.432,-5.303 18.194,-8.877c2.296,-1.424 4.526,-2.953 6.786,-4.435c2.576,-0.065 5.17,-0.355 7.723,-0.145c4.5,0.369 8.904,1.307 12.737,3.908c2.238,1.519 3.939,3.496 4.736,6.04c-0.14,0.221 -0.167,0.323 -0.227,0.349c-1.063,0.465 -2.143,0.893 -3.194,1.383c-6.055,2.821 -10.479,7.337 -13.259,13.349c-2.424,5.241 -1.382,10.402 1.047,15.386c1.19,2.441 3.62,3.714 6.339,3.293c4.946,-0.766 8.461,-3.775 11.243,-7.64c3.751,-5.209 4.589,-11.135 3.601,-17.373c-0.335,-2.116 -0.785,-4.215 -1.232,-6.582c4.976,-0.537 9.83,-1.131 14.698,-1.569c6.83,-0.614 13.517,-1.964 19.931,-4.322c7.326,-2.694 14.531,-5.719 21.76,-8.672c7.81,-3.19 14.893,-7.747 22.164,-11.949c6.273,-3.625 11.789,-8.086 15.927,-14.114c0.129,-0.188 0.372,-0.296 0.562,-0.442c0,14.833 0,29.667 0,44.5c-2.472,0.148 -4.942,0.406 -7.415,0.423c-6.998,0.048 -13.85,1.277 -20.66,2.647c-6.182,1.243 -12.155,3.319 -18.031,5.599c-4.17,1.619 -8.171,3.69 -12.377,5.197c-6.222,2.23 -11.549,6.109 -17.404,9.01c-6.341,3.141 -11.549,7.948 -17.087,12.288c-6.142,4.813 -11.179,10.666 -16.643,16.128c-2.606,2.605 -4.496,5.696 -6.591,8.626c-3.688,5.156 -7.004,10.578 -10.453,15.904c-0.214,0.329 -0.232,0.783 -0.34,1.179z" fill="#FEDBD0" id="svg_4"/>
<path d="m623,300.642881c0.108,-0.396 0.126,-0.85 0.338,-1.178c3.449,-5.325 6.766,-10.747 10.453,-15.904c2.095,-2.93 3.985,-6.021 6.591,-8.626c5.464,-5.462 10.501,-11.315 16.643,-16.128c5.538,-4.34 10.746,-9.147 17.087,-12.288c5.855,-2.901 11.182,-6.78 17.404,-9.01c4.206,-1.507 8.207,-3.578 12.377,-5.197c5.876,-2.281 11.849,-4.356 18.031,-5.599c6.81,-1.37 13.662,-2.598 20.66,-2.647c2.472,-0.017 4.943,-0.275 7.415,-0.423c0,25.667 0,51.333 0,77c-42.332,0 -84.666,0 -126.999,0zm53.375,-36.965c1.139,-0.251 2.619,-0.443 3.985,-0.943c0.563,-0.206 1.229,-1.084 1.215,-1.641c-0.013,-0.519 -0.836,-1.415 -1.355,-1.454c-2.359,-0.18 -4.741,-0.217 -7.104,-0.089c-0.962,0.052 -1.502,0.973 -1.518,1.987c-0.016,0.99 0.656,1.517 1.505,1.71c0.952,0.218 1.942,0.265 3.272,0.43zm31.44,-5.633c-1.287,0.123 -2.369,0.146 -3.416,0.349c-0.971,0.189 -1.705,0.856 -1.602,1.897c0.107,1.09 0.798,1.832 2.005,1.764c2.388,-0.135 4.776,-0.288 7.159,-0.498c0.801,-0.071 1.394,-0.547 1.424,-1.445c0.031,-0.915 -0.549,-1.353 -1.359,-1.482c-1.467,-0.232 -2.94,-0.411 -4.211,-0.585zm18.439,-17.441c1.112,-0.215 2.701,-0.371 4.167,-0.88c0.615,-0.213 1.278,-1.134 1.369,-1.807c0.134,-0.982 -0.882,-1.442 -1.701,-1.451c-2.232,-0.024 -4.478,0.013 -6.689,0.279c-0.592,0.071 -1.399,0.978 -1.532,1.621c-0.188,0.906 0.367,1.814 1.446,2.009c0.808,0.145 1.643,0.135 2.94,0.229zm-28.051,11.011c0.01,0.05 0.02,0.1 0.03,0.149c1.375,-0.248 2.772,-0.412 4.117,-0.773c0.882,-0.237 1.453,-1.041 1.088,-1.923c-0.234,-0.566 -0.959,-1.235 -1.526,-1.297c-1.551,-0.169 -3.138,-0.088 -4.705,-0.005c-0.552,0.029 -1.29,0.196 -1.585,0.577c-0.46,0.595 -0.888,1.461 -0.802,2.145c0.056,0.438 1.022,0.872 1.655,1.091c0.52,0.178 1.149,0.036 1.728,0.036zm16.873,29.366c0.595,-0.074 1.654,-0.193 2.711,-0.339c1.004,-0.139 1.888,-0.673 1.714,-1.707c-0.109,-0.646 -0.848,-1.376 -1.491,-1.706c-1.902,-0.976 -3.9,-0.309 -5.789,0.143c-0.519,0.124 -1.159,1.292 -1.087,1.904c0.073,0.618 0.87,1.313 1.518,1.651c0.518,0.27 1.289,0.054 2.424,0.054zm-62.107,-1.702c0.992,-0.141 2.333,-0.217 3.6,-0.565c0.519,-0.142 1.203,-0.862 1.214,-1.333c0.01,-0.437 -0.739,-1.228 -1.219,-1.282c-2.214,-0.249 -4.455,-0.413 -6.676,-0.329c-0.544,0.02 -1.336,1.018 -1.5,1.692c-0.23,0.941 0.638,1.49 1.508,1.636c0.896,0.15 1.823,0.117 3.073,0.181zm56.714,10.81c-0.95,0.101 -1.623,0.107 -2.263,0.255c-0.974,0.227 -1.67,0.848 -1.699,1.888c-0.03,1.088 0.736,1.636 1.698,1.861c1.885,0.441 3.648,-0.083 5.244,-1.002c0.951,-0.548 0.863,-1.725 -0.147,-2.201c-0.951,-0.447 -2.046,-0.59 -2.833,-0.801zm26.315,-37.502c0.766,-0.263 1.911,-0.507 2.907,-1.034c1.329,-0.703 1.286,-2.119 -0.137,-2.548c-1.368,-0.413 -2.888,-0.43 -4.34,-0.418c-1.058,0.008 -1.957,0.585 -1.937,1.817c0.019,1.175 0.899,1.694 1.904,1.985c0.389,0.112 0.813,0.105 1.603,0.198zm0.345,19.21c0.952,-0.19 1.982,-0.219 2.829,-0.627c0.533,-0.258 1.055,-1.068 1.089,-1.659c0.026,-0.456 -0.629,-1.207 -1.134,-1.38c-0.902,-0.31 -1.919,-0.375 -2.888,-0.376c-2.054,-0.003 -3.157,0.781 -3.079,2.046c0.086,1.385 0.91,1.871 3.183,1.996zm-51.014,9.677c1.121,-0.151 2.125,-0.188 3.072,-0.44c1.06,-0.282 1.275,-1.309 0.548,-2.125c-0.839,-0.942 -4.41,-1.793 -5.578,-1.336c-0.85,0.332 -1.374,0.897 -1.335,1.857c0.039,0.958 0.583,1.513 1.482,1.72c0.639,0.147 1.292,0.233 1.811,0.324zm54.676,11.76c-1.539,-1.969 -3.594,-2.067 -5.636,-1.96c-0.518,0.027 -1.346,0.675 -1.414,1.13c-0.09,0.604 0.254,1.474 0.712,1.921c1.518,1.481 4.853,0.946 6.338,-1.091zm-63.279,1.341c0.587,-0.147 1.677,-0.286 2.637,-0.716c0.486,-0.218 1.047,-0.983 1.011,-1.458c-0.039,-0.507 -0.658,-1.215 -1.173,-1.388c-0.918,-0.307 -1.947,-0.334 -2.934,-0.373c-1.421,-0.057 -2.583,0.753 -2.604,1.715c-0.021,1.006 1.352,2.18 3.063,2.22zm-31.358,-4.32c-0.518,0.079 -1.188,0.107 -1.81,0.293c-0.819,0.245 -1.502,0.673 -1.465,1.719c0.041,1.148 0.804,1.672 1.744,1.718c1.049,0.052 2.16,-0.065 3.152,-0.39c0.58,-0.19 1.252,-0.858 1.4,-1.436c0.245,-0.953 -0.599,-1.458 -1.413,-1.727c-0.457,-0.149 -0.975,-0.114 -1.608,-0.177z" fill="#548099" id="svg_7"/>
<path d="m346.953,290.630881c0.806,1.415 1.73,2.778 2.39,4.258c0.832,1.868 1.448,3.832 2.157,5.754c-43.5,0 -87,0 -130.5,0c0.664,-4.841 1.467,-9.669 1.949,-14.528c0.409,-4.123 0.447,-8.283 0.649,-12.426c0.655,-0.292 1.496,-0.416 1.932,-0.903c1.935,-2.161 4.26,-3.808 6.56,-5.537c3.753,-2.82 7.508,-5.604 11.694,-7.767c0.521,-0.269 0.858,-0.895 1.28,-1.356c1.969,-0.804 3.904,-1.705 5.914,-2.389c2.802,-0.953 5.66,-1.745 8.494,-2.605c0.444,0.781 0.854,1.584 1.34,2.338c1.034,1.602 3.777,1.039 4.439,-0.312c0.597,-1.219 1.348,-2.508 0.685,-3.985c1.22,-0.01 2.44,-0.019 3.659,-0.029c0.702,1.529 1.924,2.014 3.551,1.953c1.667,-0.062 1.744,-1.442 2.288,-2.46c1.394,-0.16 2.786,-0.341 4.183,-0.474c0.906,-0.086 1.406,0.366 1.468,1.282c-0.581,0.457 -1.236,0.848 -1.725,1.388c-0.997,1.1 -1.005,2.435 -0.49,3.73c0.443,1.113 1.448,1.554 2.592,1.627c1.41,0.09 3.026,-0.987 3.526,-2.301c0.455,-1.196 0.04,-2.505 -1.157,-3.529c-0.436,-0.373 -0.922,-0.689 -1.385,-1.031c0.254,-0.166 0.511,-0.479 0.762,-0.474c1.561,0.031 3.12,0.133 4.678,0.211c1.094,0.858 2.293,1.005 3.583,0.518c0.516,0.348 0.986,0.863 1.554,1.015c2.989,0.797 5.976,1.653 9.016,2.189c2.214,0.391 4.217,1.307 6.306,1.99c2.187,0.715 4.316,1.674 6.363,2.734c2.392,1.238 4.669,2.697 6.993,4.066c3.399,2.002 6.29,4.673 9.358,7.11c-0.825,0.879 -1.67,1.742 -2.471,2.642c-2.811,3.157 -5.61,6.324 -8.407,9.493c-0.931,1.055 -1.882,2.098 -2.752,3.203c-1.282,1.629 -1.073,4.988 0.312,5.917c1.948,1.307 4.248,0.857 5.65,-1.118c0.623,-0.878 1.155,-1.829 1.845,-2.649c3.565,-4.239 7.427,-8.242 10.273,-13.047c2.732,1.785 4.457,4.533 6.55,6.923c0.331,0.378 0.315,1.06 0.46,1.601c-0.496,0.652 -1.051,1.268 -1.474,1.965c-0.803,1.321 -0.775,3.932 -0.021,4.694c1.143,1.154 3.035,1.425 4.766,0.701c0.375,-0.158 0.775,-0.257 1.163,-0.382zm-72.407,-25.96c0.019,-2.027 -1.097,-3.298 -2.884,-3.284c-1.897,0.015 -4.097,2.687 -4.097,4.975c0,1.415 1.258,2.597 2.755,2.588c2.478,-0.015 4.202,-1.761 4.226,-4.279zm25.954,-5.911c0.013,-1.961 -1.734,-3.932 -3.438,-3.88c-1.956,0.061 -3.375,1.636 -3.444,3.82c-0.054,1.74 1.356,2.824 3.628,2.788c1.744,-0.026 3.244,-1.285 3.254,-2.728zm-11.518,6.038c0.038,-2.048 -0.319,-2.983 -1.43,-3.368c-1.198,-0.415 -2.281,-0.054 -3.198,0.77c-1.084,0.975 -1.515,2.591 -1.055,3.791c0.59,1.539 1.254,2.056 2.702,2.102c1.99,0.063 2.939,-0.986 2.981,-3.295z" fill="#DAF1FF" id="svg_9"/>
<path d="m703.429,159.265881c0.502,-1.426 1.928,-1.133 2.946,-1.451c1.315,-0.41 2.746,-0.447 4.128,-0.646c-0.073,1.037 -0.284,2.089 -0.18,3.108c0.102,0.994 0.945,1.465 1.891,1.687c1.031,0.242 1.717,-0.193 2.278,-1.028c0.839,-1.249 1.542,-2.52 1.195,-4.107c2.698,-0.336 5.388,-0.789 8.096,-0.966c1.798,-0.118 3.675,0.481 5.423,0.193c4.629,-0.762 9.263,-0.294 13.889,-0.384c2.297,-0.044 4.602,0.301 6.904,0.47c0,6 0,12 0,18c-0.168,0.168 -0.379,0.31 -0.499,0.507c-3.606,5.916 -8.197,10.898 -14.1,14.566c-9.541,5.927 -19.201,11.641 -29.712,15.749c-4.875,1.905 -9.688,3.974 -14.602,5.768c-6.175,2.255 -12.495,3.985 -19.078,4.65c-6.024,0.609 -12.036,1.349 -18.086,2.036c-0.09,-0.077 -0.245,-0.163 -0.332,-0.294c-0.23,-0.345 -0.451,-0.7 -0.642,-1.069c-2.945,-5.662 -7.74,-8.766 -13.872,-9.97c-2.517,-0.494 -5.059,-0.863 -7.59,-1.289c0.122,-1.386 1.372,-1.852 2.257,-2.453c3.107,-2.111 5.83,-4.582 8.397,-7.337c2.64,-2.832 5.637,-5.351 8.608,-7.852c3.084,-2.596 6.282,-5.065 9.508,-7.485c4.242,-3.181 8.981,-5.539 13.649,-8.011c1.312,-0.695 2.5,-1.622 3.798,-2.346c2.435,-1.357 4.861,-2.746 7.38,-3.931c3.057,-1.438 5.85,-3.482 9.246,-4.157c1.207,-0.24 2.427,-0.418 3.641,-0.625c0.038,0.747 0.084,1.493 0.112,2.241c0.048,1.279 0.794,1.688 1.971,1.714c3.458,0.077 4.892,-2.151 3.376,-5.288z" fill="#FEDBD0" id="svg_10"/>
<path d="m57.51,258.254881c7.145,-2.064 14.546,-3.315 21.271,-6.71c0.294,-0.148 0.596,-0.312 0.913,-0.376c1.115,-0.226 1.846,0.461 1.457,1.539c-0.502,1.392 -1.12,2.772 -1.899,4.026c-1.746,2.811 -3.623,5.542 -5.446,8.305c-3.48,3.206 -6.708,6.758 -10.503,9.534c-5.403,3.951 -11.477,6.726 -18.266,7.474c-3.769,0.415 -7.528,1.343 -11.356,0.469c-0.543,-0.124 -1.098,-0.198 -1.702,-0.304c-2.741,5.966 -4.551,12.065 -5.48,18.43c-0.667,0 -1.333,0 -2,0c0.047,-0.996 -0.041,-2.022 0.163,-2.985c1.855,-8.751 4.84,-17.099 9.313,-24.86c2.197,-3.813 4.602,-7.509 7.008,-11.196c1.63,-2.497 3.424,-4.887 5.145,-7.325c0.707,-0.793 1.414,-1.587 1.826,-2.049c-1.337,-4.444 -2.733,-8.452 -3.722,-12.558c-0.906,-3.763 -0.302,-7.588 0.281,-11.383c0.427,-2.786 0.691,-5.602 0.898,-8.414c0.539,-7.303 1.03,-14.61 1.491,-21.919c0.141,-2.242 0.072,-4.498 0.198,-6.741c0.083,-1.468 -0.072,-3.421 1.695,-3.792c1.799,-0.378 2.727,1.386 3.382,2.777c1.878,3.985 3.742,7.986 4.607,12.361c0.5,2.527 1.048,5.048 1.682,7.544c1.66,6.54 1.353,13.035 -0.341,19.504c-1.01,3.857 -2.083,7.696 -3.115,11.547c-0.081,0.302 -0.059,0.632 -0.138,1.602c4.488,-4.461 7.787,-9.186 11.478,-13.553c3.756,-4.445 7.3,-9.081 10.746,-13.773c3.441,-4.686 6.087,-9.859 8.756,-15.053c-1.95,-0.825 -3.721,-1.493 -5.418,-2.314c-1.995,-0.966 -3.474,-2.515 -4.659,-4.396c-2.918,-4.634 -4.822,-9.683 -6.084,-14.97c-1.099,-4.607 -2.018,-9.257 -2.983,-13.895c-0.37,-1.781 0.934,-2.944 2.664,-2.336c0.675,0.237 1.307,0.735 1.828,1.246c5.293,5.196 10.216,10.715 14.295,16.93c0.951,1.448 1.61,3.09 2.377,4.656c0.715,1.462 1.396,2.941 2.091,4.412c0.184,-0.025 0.369,-0.049 0.553,-0.074c0.57,-2.137 1.031,-4.311 1.741,-6.4c0.686,-2.019 0.513,-3.751 -0.461,-5.692c-1.933,-3.85 -2.263,-8.14 -2.788,-12.352c-0.267,-2.142 -0.713,-4.277 -0.791,-6.425c-0.243,-6.719 2.212,-12.722 5.395,-18.432c1.78,-3.192 3.947,-6.169 5.951,-9.235c0.226,-0.346 0.484,-0.688 0.789,-0.962c1.019,-0.913 1.987,-0.691 2.548,0.593c0.197,0.453 0.33,0.948 0.404,1.438c0.771,5.099 1.548,10.196 1.283,15.386c-0.242,4.729 -0.409,9.461 -0.621,14.191c-0.263,5.86 -1.892,11.295 -5.25,16.133c-0.69,0.994 -1.642,1.891 -2.657,2.548c-0.904,0.586 -1.382,1.11 -1.026,2.166c0.173,0.034 0.378,0.141 0.461,0.081c4.238,-3.071 9.005,-4.968 13.999,-6.39c3.119,-0.888 6.217,-1.852 9.316,-2.809c0.709,-0.219 1.533,-0.754 1.974,0.12c0.211,0.418 -0.073,1.317 -0.429,1.756c-3.838,4.724 -7.825,9.269 -13.147,12.472c-4.119,2.479 -8.495,3.238 -13.181,2.755c-0.401,-0.041 -0.801,-0.094 -1.403,-0.165c-0.513,1.27 -1.061,2.461 -1.48,3.695c-1.126,3.313 -2.51,6.49 -4.453,9.422c-0.589,0.889 -0.936,1.94 -1.393,2.917c-0.092,0.177 -0.184,0.354 -0.276,0.531c0.237,-0.025 0.475,-0.049 0.712,-0.074c8.518,-4.686 17.852,-5.489 27.302,-5.726c4.238,-0.106 8.496,0.056 12.66,-0.997c0.32,-0.081 0.688,-0.208 0.977,-0.122c0.431,0.129 0.934,0.345 1.165,0.687c0.135,0.199 -0.127,0.769 -0.346,1.076c-0.233,0.327 -0.62,0.546 -0.949,0.8c-4.471,3.456 -8.851,7.037 -13.446,10.319c-3.093,2.209 -6.334,4.341 -9.788,5.883c-4.479,1.999 -9.254,2.422 -14.099,0.841c-3.231,-1.054 -6.577,-1.775 -9.11,-4.412c-5.55,7.005 -10.925,13.789 -16.3,20.573c0.1,0.131 0.201,0.262 0.301,0.394c1.158,-0.542 2.28,-1.184 3.48,-1.603c2.105,-0.736 4.22,-1.533 6.399,-1.952c4.669,-0.897 9.384,-1.167 14.146,-0.602c2.304,0.273 4.653,0.157 6.981,0.233c0.495,0.016 1.018,0.013 1.476,0.172c0.931,0.323 1.159,1.218 0.56,1.997c-0.15,0.194 -0.348,0.364 -0.553,0.501c-5.39,3.602 -10.583,7.572 -16.826,9.66c-4.521,1.513 -9.105,2.594 -13.959,1.56c-2.984,-0.636 -6.028,-0.992 -9.407,-1.532c-1.229,1.622 -2.828,3.432 -4.06,5.465c-0.787,1.297 -1.522,2.517 -2.777,3.402c-0.313,0.22 -0.452,0.686 -0.672,1.038c-2.79,3.854 -5.6,7.693 -8.352,11.574c-0.505,0.712 -0.774,1.592 -1.151,2.396c-0.148,0.187 -0.298,0.372 -0.446,0.558c-0.049,0.137 -0.098,0.273 -0.148,0.409c0.085,-0.101 0.169,-0.202 0.254,-0.303c0.175,-0.159 0.349,-0.318 0.524,-0.476c0.359,-0.195 0.757,-0.343 1.072,-0.594c4.498,-3.578 9.142,-6.901 14.687,-8.726c0.769,-0.253 1.457,-0.754 2.182,-1.141zm-3.085,-15.668c-0.545,0.543 -0.931,1.13 -0.546,2.46c0.526,-0.893 0.821,-1.396 1.117,-1.898c-0.19,-0.188 -0.381,-0.375 -0.571,-0.562z" fill="#56819B" id="svg_11"/>
<path d="m203.5,300.642881c-28.167,0 -56.333,0 -84.5,0c1.579,-2.025 3.197,-4.021 4.731,-6.079c6.246,-8.378 11.41,-17.326 14.302,-27.453c0.29,-1.015 0.785,-1.708 1.872,-2.113c1.625,-0.605 3.261,-1.3 4.707,-2.241c2.703,-1.757 5.273,-3.719 7.899,-5.595c0.724,0.841 1.492,0.323 2.277,0.105c1.509,-0.419 3.015,-0.961 4.557,-1.123c1.518,-0.159 2.939,-0.342 4.212,-1.261c0.46,-0.332 1.027,-0.601 1.581,-0.702c3.082,-0.565 6.171,-1.099 9.267,-1.587c3.507,-0.553 7.013,-1.126 10.537,-1.545c4.593,-0.547 9.198,-1.015 13.808,-1.394c3.384,-0.278 6.784,-0.345 10.175,-0.546c0.372,-0.022 0.727,-0.309 1.091,-0.473c0.773,0.86 1.621,1.664 2.303,2.59c1.619,2.199 3.404,4.325 4.683,6.715c3.164,5.915 4.317,12.338 4.11,19.02c-1.822,1.284 -3.545,2.656 -4.742,4.6c-0.692,1.125 -1.486,2.192 -2.282,3.248c-1.578,2.094 -3.217,4.141 -4.778,6.247c-1.37,1.847 -2.75,3.695 -3.969,5.642c-0.765,1.221 -1.238,2.624 -1.841,3.945z" fill="#B2CDDD" id="svg_12"/>
<path d="m0,167.642881c1.953,0.858 4.023,1.524 5.837,2.613c4.339,2.606 8.422,5.588 12.183,9.001c7.607,6.902 12.849,15.19 15.549,25.179c1.135,4.2 1.898,8.407 2.177,12.703c0.491,7.553 -0.901,14.816 -3.643,21.862c-1.81,4.652 -4.023,9.07 -6.872,13.17c-0.178,0.257 -0.213,0.613 -0.314,0.923c-0.706,0.697 -1.407,1.399 -2.12,2.089c-1.599,1.548 -3.107,3.208 -4.825,4.612c-4.805,3.927 -9.685,7.762 -14.564,11.598c-1.008,0.792 -1.893,1.912 -3.409,1.75c0.001,-35.167 0.001,-70.333 0.001,-105.5z" fill="#54809A" id="svg_14"/>
<path d="m26.5,300.642881c0.929,-6.365 2.74,-12.464 5.48,-18.43c0.603,0.106 1.158,0.18 1.702,0.304c3.828,0.874 7.586,-0.054 11.356,-0.469c6.789,-0.748 12.863,-3.524 18.266,-7.474c3.796,-2.775 7.023,-6.328 10.503,-9.534c3.102,2.025 6.632,2.88 10.186,3.581c4.458,0.88 8.946,1.606 13.421,2.397c0.163,0.029 0.331,0.078 0.493,0.066c5.706,-0.412 11.501,0.316 17.092,-1.612c2.083,-0.719 4.354,-0.881 6.519,-1.383c0.977,-0.227 2.248,-0.094 2.553,-1.51c3.712,1.091 7.464,0.778 11.605,0.016c-0.403,1.373 -0.662,2.386 -0.997,3.374c-3.081,9.089 -8.229,17.02 -13.851,24.685c-1.456,1.986 -2.886,3.991 -4.328,5.988c-30,0.001 -60,0.001 -90,0.001z" fill="#B2CDDD" id="svg_15"/>
<path d="m453.697,248.860881c-1.036,1.466 -1.992,2.999 -3.127,4.385c-1.997,2.439 -3.893,4.933 -5.48,7.668c-0.741,1.277 -1.777,2.387 -1.938,3.947c-0.025,0.244 -0.081,0.511 -0.207,0.716c-2.923,4.751 -4.145,10.224 -6.466,15.235c-0.896,1.934 -1.377,4.067 -1.979,6.129c-0.922,3.158 -1.861,6.316 -2.636,9.511c-0.327,1.347 -0.255,2.791 -0.365,4.191c-8.333,0 -16.667,0 -25,0c-0.064,-0.492 -0.127,-0.984 -0.191,-1.476c-0.786,-6.001 -1.833,-11.982 -2.289,-18.008c-0.5,-6.618 -0.296,-13.268 0.94,-19.837c1.29,-6.857 3.538,-13.302 7.994,-18.814c1.804,-2.231 3.905,-4.056 6.617,-5.126c7.006,-2.763 13.85,-2.131 20.494,1.109c5.215,2.544 9.46,6.425 13.633,10.37z" fill="#FEF6F3" id="svg_18"/>
<path d="m453.697,248.860881c-4.173,-3.945 -8.418,-7.826 -13.632,-10.369c-6.644,-3.241 -13.488,-3.872 -20.494,-1.109c-2.712,1.069 -4.814,2.894 -6.617,5.126c-4.456,5.512 -6.704,11.957 -7.994,18.814c-1.236,6.569 -1.44,13.219 -0.94,19.837c0.456,6.026 1.503,12.007 2.289,18.008c0.064,0.492 0.128,0.984 0.191,1.476c-1,0 -2,0 -3,0c-0.048,-0.899 -0.047,-1.804 -0.152,-2.696c-0.636,-5.436 -1.441,-10.857 -1.901,-16.307c-0.335,-3.965 -0.414,-7.975 -0.272,-11.952c0.276,-7.714 1.882,-15.169 5.327,-22.127c2.157,-4.356 4.886,-8.252 9.027,-11.079c4.653,-3.177 9.791,-4.007 15.208,-3.599c7.091,0.533 12.977,3.859 18.324,8.305c2.236,1.859 4.459,3.733 6.689,5.599c3.79,3.227 7.337,6.807 11.421,9.605c7.432,5.091 15.641,6.85 24.394,3.67c4.21,-1.529 8.286,-3.37 12.08,-5.845c5.415,-3.533 10.19,-7.729 14.251,-12.749c0.603,-0.745 1.155,-1.531 1.799,-2.388c-4.865,-3.29 -8.586,-7.267 -11.563,-11.948c-1.977,-4.036 -3.754,-8.144 -4.246,-12.674c-0.618,-5.684 0.519,-10.88 4.306,-15.343c7.45,-8.777 17.131,-4.118 20.673,1.989c3.019,5.207 4.006,10.856 2.952,16.788c-0.522,2.935 -1.291,5.827 -1.948,8.738c-1.193,2.637 -2.33,5.303 -3.604,7.9c-0.639,1.302 -1.527,2.482 -2.352,3.795c1.172,0.621 2.086,1.221 3.08,1.615c10.465,4.143 20.984,4.634 31.595,0.488c5.979,-2.336 11.681,-5.231 17.151,-8.551c3.608,-2.19 6.999,-4.739 10.487,-7.127c2.397,-1.768 4.815,-3.507 7.186,-5.31c7.254,-5.519 15.14,-9.814 23.882,-12.49c3.984,-1.219 8.052,-1.862 12.21,-1.993c0.661,-0.021 1.321,-0.104 1.981,-0.157c2.531,0.427 5.073,0.795 7.59,1.29c6.133,1.204 10.928,4.308 13.872,9.97c0.191,0.368 0.411,0.723 0.642,1.069c0.087,0.131 0.242,0.217 0.332,0.294c6.05,-0.686 12.062,-1.426 18.086,-2.036c6.583,-0.666 12.902,-2.396 19.078,-4.65c4.914,-1.794 9.728,-3.863 14.602,-5.768c10.511,-4.108 20.171,-9.822 29.712,-15.749c5.903,-3.667 10.494,-8.65 14.1,-14.566c0.12,-0.197 0.331,-0.339 0.499,-0.507c0,1.667 0,3.333 0,5c-0.19,0.146 -0.434,0.254 -0.562,0.442c-4.137,6.029 -9.654,10.489 -15.927,14.114c-7.271,4.202 -14.354,8.758 -22.164,11.949c-7.229,2.953 -14.434,5.979 -21.76,8.672c-6.414,2.358 -13.101,3.708 -19.931,4.322c-4.868,0.438 -9.722,1.032 -14.698,1.569c0.447,2.367 0.896,4.466 1.232,6.582c0.989,6.238 0.15,12.164 -3.601,17.373c-2.782,3.864 -6.297,6.874 -11.243,7.64c-2.72,0.421 -5.149,-0.852 -6.339,-3.293c-2.429,-4.984 -3.471,-10.145 -1.047,-15.386c2.78,-6.012 7.204,-10.527 13.259,-13.349c1.051,-0.49 2.131,-0.918 3.194,-1.383c0.06,-0.026 0.087,-0.129 0.227,-0.349c-0.797,-2.545 -2.498,-4.521 -4.736,-6.04c-3.833,-2.602 -8.237,-3.539 -12.737,-3.908c-2.553,-0.209 -5.147,0.08 -7.722,0.145c-2.839,0.556 -5.759,0.858 -8.5,1.723c-5.473,1.727 -10.671,4.173 -15.496,7.29c-2.762,1.783 -5.307,3.901 -7.95,5.869c-0.817,0.561 -1.677,1.07 -2.446,1.692c-9.188,7.432 -19.365,13.198 -30.219,17.776c-5.006,2.111 -10.247,3.548 -15.701,3.762c-6.311,0.247 -12.404,-0.995 -18.246,-3.4c-1.536,-0.632 -3.032,-1.364 -4.737,-2.135c-0.982,1.112 -1.995,2.19 -2.932,3.33c-5.141,6.249 -11.288,11.234 -18.465,14.989c-3.85,2.014 -7.804,3.629 -12.041,4.69c-7.43,1.86 -14.189,0.184 -20.6,-3.493c-4.347,-2.493 -8.189,-5.684 -11.787,-9.17c-0.834,-0.816 -1.748,-1.545 -2.626,-2.31zm73.068,-22.082c1.719,-5.217 2.952,-10.54 2.417,-16.06c-0.418,-4.322 -1.932,-8.262 -5.474,-11.091c-3.886,-3.104 -8.374,-3.952 -12.441,0.361c-3.003,3.185 -4.691,7.101 -4.647,11.502c0.057,5.679 1.696,10.943 5.001,15.629c2.474,4.007 5.76,7.16 9.901,9.719c2.297,-3.261 4.164,-6.449 5.243,-10.06zm125.196,-5.674c-5.95,1.56 -10.071,5.141 -13.571,9.623c-2.34,2.996 -2.873,6.522 -2.83,10.114c0.026,2.222 0.639,4.441 1.956,6.347c1.279,1.852 2.618,2.387 4.71,1.598c1.375,-0.518 2.773,-1.183 3.906,-2.097c6.705,-5.413 8.435,-12.613 7.132,-20.786c-0.256,-1.601 -0.846,-3.15 -1.303,-4.799z" fill="#5D859D" id="svg_20"/>
<path d="m0,273.142881c1.516,0.161 2.401,-0.958 3.409,-1.75c4.879,-3.836 9.759,-7.671 14.564,-11.598c1.717,-1.404 3.226,-3.064 4.825,-4.612c0.713,-0.69 1.414,-1.392 2.12,-2.089c0.999,0.956 2.174,0.507 3.297,0.397c2.798,-0.273 5.591,-0.608 8.392,-0.847c3.306,-0.282 6.367,0.969 9.522,1.631c-1.72,2.438 -3.515,4.828 -5.144,7.325c-2.406,3.687 -4.811,7.384 -7.008,11.196c-4.473,7.761 -7.457,16.109 -9.313,24.86c-0.204,0.963 -0.116,1.988 -0.163,2.985c-8.167,0 -16.333,0 -24.5,0c-0.001,-9.165 -0.001,-18.331 -0.001,-27.498z" fill="#B2CDDD" id="svg_24"/>
<path d="m116.5,300.642881c1.442,-1.996 2.872,-4.002 4.328,-5.988c5.622,-7.665 10.77,-15.596 13.851,-24.685c0.335,-0.988 0.594,-2.002 0.997,-3.374c-4.141,0.762 -7.892,1.075 -11.605,-0.016c-2.444,-2.027 -4.795,-4.112 -5.824,-7.288c-2.189,-6.757 -0.875,-12.83 3.686,-18.161c2.091,-2.444 5.436,-2.988 8.48,-1.804c4.269,1.661 6.988,4.729 8.105,9.135c0.943,3.719 1.461,7.501 0.999,11.362c-0.087,0.726 -0.013,1.471 -0.013,2.42c0.622,-0.123 1.191,-0.128 1.66,-0.349c1.127,-0.53 2.259,-1.079 3.295,-1.764c5.63,-3.723 11.032,-7.753 15.879,-12.467c8.534,-8.302 18.932,-11.766 30.552,-10.834c8.313,0.666 15.79,3.901 21.214,10.745c0.508,0.641 1.185,1.147 1.784,1.715c1.658,2.364 3.474,4.635 4.939,7.113c3.137,5.308 4.259,11.225 4.77,17.286c-0.202,4.143 -0.24,8.303 -0.649,12.426c-0.482,4.859 -1.285,9.687 -1.949,14.528c-0.667,0 -1.333,0 -2,0c0.064,-0.902 0.077,-1.811 0.199,-2.704c0.954,-6.964 1.934,-13.924 1.911,-20.977c0.208,-6.682 -0.946,-13.105 -4.11,-19.02c-1.279,-2.39 -3.064,-4.516 -4.683,-6.715c-0.682,-0.926 -1.53,-1.731 -2.303,-2.59c-1.733,-2.164 -3.651,-4.181 -6.227,-5.273c-3.279,-1.39 -6.602,-2.749 -10.019,-3.724c-4.067,-1.161 -8.247,-0.643 -12.392,-0.074c-6.704,0.92 -12.55,3.703 -17.688,8.031c-3.75,3.16 -7.454,6.375 -11.178,9.566c-2.626,1.876 -5.196,3.838 -7.899,5.595c-1.447,0.941 -3.082,1.635 -4.707,2.241c-1.087,0.405 -1.582,1.099 -1.872,2.113c-2.892,10.127 -8.056,19.075 -14.302,27.453c-1.535,2.058 -3.152,4.055 -4.731,6.079c-0.831,-0.001 -1.665,-0.001 -2.498,-0.001zm21.085,-44.076c-0.226,-1.841 -0.346,-3.67 -0.69,-5.455c-0.702,-3.64 -2.383,-6.701 -5.587,-8.784c-3.248,-2.112 -6.703,-1.67 -8.715,1.513c-2.711,4.287 -3.974,9.005 -2.481,14.042c1.149,3.876 3.627,6.344 7.888,6.96c2.418,0.349 4.596,-0.35 6.873,-0.68c1.263,-0.183 1.866,-0.953 2.018,-2.163c0.228,-1.812 0.463,-3.622 0.694,-5.433z" fill="#5E879F" id="svg_33"/>
<path d="m221.11,276.961881c0.023,7.053 -0.957,14.013 -1.911,20.977c-0.123,0.894 -0.136,1.803 -0.199,2.704c-5.167,0 -10.333,0 -15.5,0c0.603,-1.321 1.076,-2.724 1.839,-3.944c1.218,-1.946 2.599,-3.795 3.969,-5.642c1.561,-2.105 3.2,-4.153 4.778,-6.247c0.796,-1.057 1.59,-2.123 2.282,-3.248c1.197,-1.945 2.92,-3.316 4.742,-4.6z" fill="#DAF1FF" id="svg_34"/>
<path d="m508.133,227.129881c2.977,4.682 6.698,8.658 11.563,11.948c-0.643,0.857 -1.196,1.643 -1.799,2.388c-4.06,5.019 -8.836,9.216 -14.251,12.749c-3.794,2.475 -7.869,4.315 -12.08,5.845c-8.754,3.18 -16.962,1.421 -24.394,-3.67c-4.084,-2.797 -7.631,-6.378 -11.421,-9.605c0.51,-0.232 1.208,-0.327 1.499,-0.721c1.406,-1.904 3.562,-2.659 5.458,-3.826c0.553,-0.341 0.885,-1.041 1.319,-1.576c2.188,-1.133 4.354,-2.31 6.567,-3.391c5.425,-2.651 10.837,-5.319 16.666,-7.044c1.654,-0.49 3.333,-0.605 4.974,-0.974c5.244,-1.179 10.582,-1.552 15.899,-2.123z" fill="#FEDBD0" id="svg_39"/>
<path d="m586.228,224.746881c-3.488,2.388 -6.878,4.937 -10.487,7.127c-5.47,3.32 -11.172,6.215 -17.151,8.551c-10.612,4.146 -21.13,3.656 -31.595,-0.488c-0.995,-0.394 -1.908,-0.993 -3.08,-1.615c0.824,-1.313 1.713,-2.492 2.352,-3.795c1.274,-2.598 2.411,-5.263 3.604,-7.9c5.575,0.006 11.151,0.061 16.726,-0.003c2.524,-0.029 5.044,-0.372 7.568,-0.427c1.561,-0.034 3.128,0.21 4.693,0.321c0.579,0.041 1.161,0.112 1.738,0.083c2.645,-0.133 5.286,-0.381 7.931,-0.43c5.194,-0.097 10.349,-0.52 15.467,-1.418c0.723,-0.127 1.488,-0.01 2.234,-0.006z" fill="#FEDAD0" id="svg_40"/>
<path d="m595.508,185.574881c1.815,-0.428 3.265,-0.768 4.714,-1.114c1.426,-0.341 2.629,-0.045 3.518,1.197c0.932,1.301 0.614,2.648 -0.249,3.691c-1.258,1.521 -2.76,2.843 -4.187,4.221c-0.475,0.459 -1.03,0.837 -1.562,1.234c-0.678,0.505 -1.005,1.152 -0.635,1.948c0.77,1.659 0.227,2.937 -1.15,3.892c-1.168,0.81 -2.502,0.376 -4.104,-1.156c-0.049,-0.047 -0.145,-0.046 -0.414,-0.123c-1.114,0.772 -2.349,1.596 -3.549,2.469c-1.042,0.758 -2.035,0.944 -3.203,0.204c-1.139,-0.721 -1.505,-1.711 -1.397,-2.964c0.128,-1.487 0.271,-2.972 0.428,-4.683c-0.666,-0.273 -1.329,-0.554 -1.999,-0.816c-0.692,-0.271 -1.439,-0.442 -2.076,-0.806c-0.871,-0.497 -1.611,-2.212 -1.29,-3.033c0.402,-1.03 1.054,-1.877 2.306,-2.007c1.484,-0.154 2.961,-0.413 4.449,-0.496c1.31,-0.073 2.229,-0.56 2.663,-1.83c0.614,-1.798 1.3,-3.576 1.818,-5.402c0.548,-1.934 1.235,-2.706 2.722,-2.679c1.553,0.028 2.468,0.811 2.643,2.367c0.21,1.888 0.358,3.784 0.554,5.886z" fill="#57829B" id="svg_41"/>
<path d="m715.687,156.829881c0.347,1.587 -0.356,2.857 -1.195,4.107c-0.56,0.834 -1.247,1.27 -2.278,1.028c-0.946,-0.222 -1.79,-0.693 -1.891,-1.687c-0.104,-1.019 0.107,-2.071 0.18,-3.108c0.053,-0.063 0.104,-0.129 0.16,-0.19c2.162,-2.296 2.731,-2.313 5.024,-0.15z" fill="#648CA3" id="svg_57"/>
<path d="m706.431,153.888881c-1.644,0.036 -3.161,-1.439 -3.192,-3.104c-0.028,-1.499 1.198,-2.548 3.001,-2.567c1.618,-0.017 3.153,1.284 3.199,2.789c0.058,1.866 -1.115,2.593 -3.008,2.882z" fill="#6F92A7" id="svg_58"/>
<path d="m703.429,159.265881c1.516,3.137 0.082,5.365 -3.374,5.289c-1.177,-0.026 -1.923,-0.435 -1.971,-1.714c-0.028,-0.747 -0.074,-1.493 -0.112,-2.241c1.107,-1.124 2.482,-1.565 4.025,-1.576c0.175,0.01 0.35,0.019 0.525,0.029c0.302,0.072 0.604,0.142 0.907,0.213z" fill="#6F92A7" id="svg_61"/>
<path d="m702.521,159.053881c-0.175,-0.009 -0.35,-0.019 -0.525,-0.029c0.175,0.009 0.35,0.019 0.525,0.029z" fill="#FEDBD0" id="svg_63"/>
<path d="m223.597,273.687881c-0.512,-6.061 -1.633,-11.978 -4.77,-17.286c-1.465,-2.479 -3.282,-4.749 -4.939,-7.113c4.654,-0.328 9.281,-0.385 13.788,1.146c0.932,0.317 1.925,0.51 2.906,0.615c4.351,0.469 7.965,2.621 11.503,4.953c1.017,0.67 1.987,1.413 2.979,2.123c-0.421,0.461 -0.758,1.086 -1.279,1.356c-4.186,2.163 -7.941,4.946 -11.694,7.767c-2.3,1.728 -4.625,3.375 -6.56,5.537c-0.438,0.486 -1.279,0.611 -1.934,0.902z" fill="#B2CDDD" id="svg_64"/>
<path d="m335.508,275.129881c-2.846,4.805 -6.708,8.808 -10.273,13.047c-0.69,0.82 -1.222,1.771 -1.845,2.649c-1.402,1.974 -3.702,2.424 -5.65,1.118c-1.385,-0.929 -1.593,-4.288 -0.312,-5.917c0.87,-1.105 1.82,-2.147 2.752,-3.203c2.797,-3.169 5.596,-6.336 8.407,-9.493c0.802,-0.9 1.646,-1.763 2.471,-2.642c0.73,-0.128 1.455,-0.312 2.191,-0.373c1.829,-0.152 2.838,0.825 2.697,2.636c-0.056,0.733 -0.286,1.452 -0.438,2.178z" fill="#B4CFDE" id="svg_66"/>
<path d="m365.213,292.639881c-2.923,0.011 -4.061,-1.133 -3.662,-3.836c0.188,-1.269 0.596,-2.692 1.385,-3.648c2.256,-2.732 4.501,-5.508 7.436,-7.601c2.437,-1.738 4.98,-1.037 5.309,1.618c0.102,0.82 -0.158,1.866 -0.631,2.545c-2.263,3.244 -4.637,6.413 -7.017,9.575c-0.726,0.965 -1.689,1.614 -2.82,1.347z" fill="#B7D0DE" id="svg_68"/>
<path d="m345.5,268.101881c-2.654,0.057 -4.406,-1.231 -4.777,-3.208c-0.12,-0.641 0.009,-1.577 0.414,-2.029c2.596,-2.899 5.285,-5.716 7.986,-8.519c0.445,-0.463 1.105,-0.714 1.651,-1.085c1.131,-0.771 2.18,-0.518 3.214,0.206c1.045,0.731 1.135,1.807 0.959,2.898c-0.359,2.215 -1.79,3.89 -3.09,5.585c-1.255,1.636 -2.622,3.204 -4.082,4.659c-0.761,0.758 -1.828,1.209 -2.275,1.493z" fill="#B7D0DE" id="svg_69"/>
<path d="m362.953,268.123881c-2.726,-0.059 -4.533,-2.69 -3.744,-5.101c0.43,-1.314 1.096,-2.661 2.005,-3.683c1.368,-1.538 2.89,-3.059 4.636,-4.101c1.254,-0.749 3.001,-1.022 4.494,-0.932c1.821,0.11 2.8,2.638 1.747,4.331c-0.991,1.594 -1.889,3.258 -3.578,4.394c-1.045,0.703 -1.697,2 -2.499,3.05c-0.892,1.168 -1.963,2 -3.061,2.042z" fill="#B7D0DE" id="svg_70"/>
<path d="m346.953,290.630881c-0.389,0.126 -0.788,0.224 -1.164,0.382c-1.732,0.724 -3.623,0.453 -4.766,-0.701c-0.755,-0.762 -0.782,-3.373 0.021,-4.694c0.423,-0.696 0.978,-1.312 1.474,-1.965c0.473,-0.289 1.11,-0.468 1.391,-0.887c1.734,-2.582 4.36,-3.985 6.856,-5.494c2.881,1.421 3.464,2.328 2.74,4.45c-0.288,0.844 -0.712,1.688 -1.258,2.389c-1.717,2.21 -3.522,4.352 -5.294,6.52z" fill="#B7D0DE" id="svg_71"/>
<path d="m57.51,258.254881c-0.725,0.387 -1.413,0.888 -2.182,1.141c-5.545,1.826 -10.189,5.148 -14.687,8.726c-0.315,0.251 -0.713,0.398 -1.073,0.593c-0.061,-0.064 -0.122,-0.126 -0.184,-0.188c0.378,-0.802 0.647,-1.682 1.152,-2.394c2.752,-3.881 5.562,-7.72 8.352,-11.574c0.196,1.12 1.068,0.99 1.89,1.103c2.438,0.336 4.816,0.879 6.732,2.593z" fill="#B2CDDD" id="svg_72"/>
<path d="m270.464,237.841881c-0.074,1.702 -1.733,3.845 -3.338,4.195c-1.004,0.219 -1.98,0.172 -2.694,-0.786c-0.887,-1.19 -1.42,-2.497 -0.845,-3.92c0.328,-0.812 0.93,-1.582 1.586,-2.177c0.994,-0.901 2.15,-1.678 3.602,-0.953c1.113,0.558 1.759,2.025 1.689,3.641z" fill="#5E879F" id="svg_77"/>
<path d="m286.407,233.645881c-0.038,2.102 -1.875,3.799 -4.097,3.782c-1.9,-0.014 -3.265,-1.56 -3.191,-3.616c0.07,-1.971 2.163,-4.021 4.041,-3.957c1.579,0.054 3.278,2.038 3.247,3.791z" fill="#5D859D" id="svg_79"/>
<path d="m265.936,251.172881c0.663,1.477 -0.088,2.766 -0.685,3.985c-0.662,1.351 -3.405,1.914 -4.439,0.312c-0.486,-0.754 -0.896,-1.557 -1.34,-2.338c0.027,-0.161 0.077,-0.321 0.078,-0.482c0.005,-1.561 0.793,-2.748 2.171,-3.216c1.382,-0.47 2.73,0.014 3.716,1.205c0.155,0.188 0.332,0.357 0.499,0.534z" fill="#5E879F" id="svg_81"/>
<path d="m298.765,241.316881c1.714,-0.007 3.078,1.376 3.141,3.186c0.065,1.862 -1.575,3.436 -3.519,3.378c-1.716,-0.052 -3.134,-1.535 -3.102,-3.246c0.029,-1.599 1.824,-3.311 3.48,-3.318z" fill="#5E879F" id="svg_82"/>
<path d="m282.445,251.329881c0.463,0.342 0.949,0.658 1.385,1.031c1.197,1.024 1.613,2.333 1.157,3.529c-0.5,1.314 -2.117,2.391 -3.526,2.301c-1.144,-0.073 -2.149,-0.514 -2.592,-1.627c-0.515,-1.295 -0.507,-2.629 0.49,-3.73c0.49,-0.54 1.145,-0.931 1.725,-1.388c0.454,-0.038 0.908,-0.077 1.361,-0.116z" fill="#5E879F" id="svg_83"/>
<path d="m275.434,250.637881c-0.544,1.018 -0.621,2.398 -2.288,2.46c-1.627,0.06 -2.849,-0.425 -3.551,-1.953c-0.009,-0.574 -0.08,-1.155 -0.016,-1.721c0.219,-1.933 1.62,-3.182 3.424,-3.108c1.562,0.064 2.446,1.248 2.477,3.336c0.005,0.328 -0.03,0.657 -0.046,0.986z" fill="#648CA3" id="svg_85"/>
<path d="m291.468,251.583881c-1.29,0.487 -2.489,0.34 -3.583,-0.518c-0.97,-2.772 -0.614,-3.836 1.427,-5.153c1.211,-0.782 2.179,-0.419 3.161,0.28c1.001,0.713 1.325,1.786 0.845,2.838c-0.429,0.938 -1.219,1.709 -1.85,2.553z" fill="#648CA3" id="svg_86"/>
<path d="m281.534,241.295881c1.544,-0.012 2.727,1.275 2.695,2.933c-0.03,1.558 -1.463,2.836 -3.168,2.825c-1.485,-0.01 -2.853,-1.174 -2.926,-2.491c-0.089,-1.609 1.621,-3.253 3.399,-3.267z" fill="#648CA3" id="svg_87"/>
<path d="m293.736,231.754881c1.644,0.208 2.646,1.505 2.404,3.111c-0.277,1.838 -1.463,3.036 -2.797,2.827c-1.772,-0.278 -3.009,-1.846 -2.752,-3.487c0.254,-1.615 1.58,-2.649 3.145,-2.451z" fill="#648CA3" id="svg_88"/>
<path d="m593.498,223.123881c2.643,-1.967 5.188,-4.085 7.95,-5.869c4.826,-3.116 10.023,-5.563 15.496,-7.29c2.742,-0.865 5.661,-1.167 8.5,-1.723c-2.26,1.482 -4.49,3.011 -6.786,4.435c-5.762,3.575 -11.658,6.908 -18.194,8.877c-2.273,0.686 -4.641,1.056 -6.966,1.57z" fill="#FDFDFE" id="svg_122"/>
<path d="m676.375,263.677881c-1.33,-0.165 -2.321,-0.213 -3.273,-0.429c-0.848,-0.193 -1.521,-0.721 -1.505,-1.71c0.016,-1.015 0.557,-1.935 1.518,-1.987c2.362,-0.128 4.745,-0.09 7.104,0.089c0.519,0.039 1.342,0.936 1.355,1.454c0.014,0.556 -0.651,1.434 -1.215,1.641c-1.365,0.5 -2.844,0.691 -3.984,0.942z" fill="#EDD1CA" id="svg_131"/>
<path d="m707.815,258.044881c1.271,0.174 2.745,0.353 4.21,0.585c0.81,0.129 1.39,0.567 1.359,1.482c-0.03,0.897 -0.622,1.374 -1.424,1.445c-2.382,0.211 -4.77,0.363 -7.159,0.498c-1.207,0.068 -1.898,-0.673 -2.005,-1.764c-0.103,-1.041 0.632,-1.708 1.602,-1.897c1.047,-0.203 2.13,-0.226 3.417,-0.349z" fill="#EDD1CA" id="svg_132"/>
<path d="m726.254,240.603881c-1.297,-0.094 -2.132,-0.084 -2.94,-0.23c-1.079,-0.195 -1.634,-1.103 -1.446,-2.009c0.133,-0.642 0.94,-1.549 1.532,-1.621c2.211,-0.265 4.457,-0.303 6.689,-0.279c0.819,0.009 1.835,0.469 1.701,1.451c-0.092,0.674 -0.755,1.594 -1.369,1.807c-1.466,0.51 -3.055,0.666 -4.167,0.881z" fill="#EDD1CA" id="svg_133"/>
<path d="m698.203,251.614881c-0.58,0 -1.209,0.142 -1.727,-0.036c-0.633,-0.219 -1.599,-0.652 -1.655,-1.091c-0.087,-0.684 0.341,-1.55 0.802,-2.145c0.295,-0.381 1.033,-0.548 1.585,-0.577c1.567,-0.083 3.154,-0.164 4.705,0.005c0.566,0.062 1.292,0.731 1.526,1.297c0.365,0.882 -0.206,1.686 -1.088,1.923c-1.345,0.361 -2.742,0.525 -4.117,0.773c-0.01,-0.05 -0.02,-0.1 -0.031,-0.149z" fill="#EDD1CA" id="svg_134"/>
<path d="m715.076,280.980881c-1.135,0 -1.906,0.215 -2.424,-0.055c-0.648,-0.338 -1.446,-1.033 -1.518,-1.651c-0.072,-0.612 0.568,-1.78 1.087,-1.904c1.89,-0.452 3.887,-1.119 5.789,-0.143c0.643,0.33 1.382,1.06 1.491,1.706c0.174,1.034 -0.71,1.569 -1.714,1.707c-1.056,0.147 -2.116,0.266 -2.711,0.34z" fill="#EDD1CA" id="svg_135"/>
<path d="m652.969,279.278881c-1.25,-0.064 -2.178,-0.031 -3.074,-0.181c-0.87,-0.146 -1.738,-0.696 -1.508,-1.636c0.165,-0.674 0.957,-1.671 1.5,-1.692c2.221,-0.083 4.462,0.08 6.676,0.329c0.48,0.054 1.229,0.845 1.219,1.282c-0.011,0.471 -0.695,1.191 -1.214,1.333c-1.265,0.348 -2.607,0.425 -3.599,0.565z" fill="#EDD1CA" id="svg_136"/>
<path d="m709.683,290.088881c0.787,0.211 1.882,0.354 2.833,0.801c1.011,0.476 1.098,1.653 0.147,2.201c-1.596,0.919 -3.36,1.443 -5.244,1.002c-0.962,-0.225 -1.728,-0.773 -1.698,-1.861c0.029,-1.04 0.724,-1.662 1.699,-1.888c0.64,-0.148 1.313,-0.154 2.263,-0.255z" fill="#EDD1CA" id="svg_137"/>
<path d="m735.998,252.586881c-0.79,-0.092 -1.214,-0.085 -1.604,-0.198c-1.005,-0.291 -1.885,-0.81 -1.904,-1.985c-0.02,-1.232 0.879,-1.808 1.937,-1.817c1.452,-0.012 2.972,0.005 4.34,0.418c1.423,0.429 1.466,1.845 0.137,2.548c-0.995,0.527 -2.14,0.771 -2.906,1.034z" fill="#EDD1CA" id="svg_138"/>
<path d="m736.343,271.796881c-2.273,-0.125 -3.097,-0.611 -3.183,-1.997c-0.078,-1.265 1.025,-2.049 3.079,-2.046c0.969,0.001 1.987,0.067 2.888,0.376c0.505,0.173 1.16,0.925 1.134,1.38c-0.034,0.591 -0.556,1.402 -1.089,1.659c-0.846,0.409 -1.876,0.438 -2.829,0.628z" fill="#EDD1CA" id="svg_139"/>
<path d="m685.329,281.473881c-0.519,-0.091 -1.172,-0.177 -1.811,-0.324c-0.899,-0.207 -1.443,-0.763 -1.482,-1.72c-0.039,-0.96 0.485,-1.524 1.335,-1.857c1.168,-0.457 4.739,0.394 5.578,1.336c0.727,0.816 0.512,1.843 -0.548,2.125c-0.947,0.252 -1.951,0.289 -3.072,0.44z" fill="#EDD1CA" id="svg_140"/>
<path d="m740.005,293.233881c-1.484,2.038 -4.82,2.573 -6.338,1.091c-0.458,-0.447 -0.802,-1.318 -0.712,-1.921c0.068,-0.455 0.896,-1.103 1.414,-1.13c2.041,-0.107 4.097,-0.01 5.636,1.96z" fill="#EDD1CA" id="svg_141"/>
<path d="m676.726,294.574881c-1.711,-0.04 -3.084,-1.214 -3.063,-2.22c0.021,-0.962 1.182,-1.772 2.604,-1.715c0.987,0.039 2.016,0.066 2.934,0.373c0.515,0.172 1.134,0.88 1.173,1.388c0.036,0.475 -0.525,1.241 -1.011,1.458c-0.96,0.43 -2.05,0.569 -2.637,0.716z" fill="#EDD1CA" id="svg_142"/>
<path d="m645.368,290.254881c0.633,0.063 1.151,0.028 1.608,0.179c0.814,0.269 1.658,0.774 1.413,1.727c-0.148,0.577 -0.82,1.246 -1.4,1.436c-0.992,0.324 -2.103,0.442 -3.152,0.39c-0.94,-0.046 -1.703,-0.571 -1.744,-1.718c-0.038,-1.047 0.646,-1.474 1.465,-1.719c0.622,-0.188 1.291,-0.216 1.81,-0.295z" fill="#EDD1CA" id="svg_143"/>
<path d="m274.546,264.670881c-0.023,2.518 -1.747,4.264 -4.225,4.279c-1.497,0.009 -2.755,-1.173 -2.755,-2.588c0,-2.289 2.2,-4.961 4.097,-4.975c1.785,-0.014 2.901,1.257 2.883,3.284z" fill="#5C869F" id="svg_145"/>
<path d="m300.5,258.759881c-0.01,1.443 -1.51,2.701 -3.254,2.729c-2.272,0.036 -3.682,-1.048 -3.628,-2.788c0.068,-2.185 1.488,-3.76 3.444,-3.82c1.704,-0.053 3.451,1.919 3.438,3.879z" fill="#5C869F" id="svg_146"/>
<path d="m288.982,264.797881c-0.042,2.309 -0.991,3.358 -2.981,3.296c-1.448,-0.045 -2.113,-0.562 -2.702,-2.102c-0.46,-1.2 -0.029,-2.816 1.055,-3.791c0.917,-0.825 2,-1.185 3.198,-0.77c1.111,0.384 1.468,1.319 1.43,3.367z" fill="#5E879F" id="svg_147"/>
<path d="m54.996,243.147881c-0.296,0.503 -0.591,1.005 -1.117,1.898c-0.385,-1.33 0.001,-1.917 0.546,-2.46c0.191,0.188 0.381,0.375 0.571,0.562z" fill="#B7D0DE" id="svg_148"/>
<path d="m54.996,243.147881c-0.19,-0.187 -0.381,-0.375 -0.571,-0.562c0.19,0.188 0.381,0.375 0.571,0.562z" fill="#FFF6F3" id="svg_149"/>
<path d="m85.732,206.903881c-0.237,0.025 -0.475,0.049 -0.712,0.074c0.092,-0.177 0.185,-0.353 0.276,-0.531c0.145,0.152 0.291,0.304 0.436,0.457z" fill="#FFF6F3" id="svg_150"/>
<path d="m38.938,269.085881c0.15,-0.185 0.299,-0.37 0.447,-0.557c0.061,0.06 0.122,0.123 0.183,0.187c-0.174,0.159 -0.349,0.318 -0.523,0.477l-0.107,-0.107z" fill="#B2CDDD" id="svg_151"/>
<path d="m39.045,269.192881c-0.085,0.101 -0.169,0.202 -0.254,0.303c0.049,-0.136 0.099,-0.272 0.148,-0.409c-0.001,-0.001 0.106,0.106 0.106,0.106z" fill="#B2CDDD" id="svg_152"/>
<path d="m152.512,257.161881c3.724,-3.191 7.427,-6.406 11.178,-9.566c5.138,-4.329 10.983,-7.111 17.688,-8.031c4.144,-0.569 8.325,-1.086 12.392,0.074c3.417,0.975 6.74,2.334 10.019,3.724c2.576,1.092 4.493,3.109 6.227,5.273c-0.363,0.164 -0.719,0.451 -1.09,0.473c-3.391,0.201 -6.791,0.268 -10.175,0.546c-4.61,0.379 -9.215,0.848 -13.808,1.394c-3.524,0.419 -7.03,0.992 -10.537,1.545c-3.095,0.488 -6.184,1.022 -9.267,1.587c-0.554,0.102 -1.122,0.37 -1.581,0.702c-1.272,0.919 -2.694,1.102 -4.212,1.261c-1.542,0.161 -3.047,0.703 -4.557,1.123c-0.786,0.218 -1.553,0.736 -2.277,-0.105z" fill="#FCF4F2" id="svg_153"/>
<path d="m511.621,227.118881c-3.305,-4.685 -4.944,-9.95 -5.001,-15.629c-0.044,-4.401 1.644,-8.317 4.647,-11.502c4.067,-4.313 8.555,-3.465 12.441,-0.361c3.542,2.83 5.056,6.77 5.474,11.091c0.534,5.52 -0.699,10.843 -2.418,16.059c-0.983,-0.041 -1.967,-0.142 -2.947,-0.111c-4.066,0.131 -8.131,0.299 -12.196,0.453z" fill="#FDFDFE" id="svg_154"/>
<path d="m651.961,221.104881c0.457,1.649 1.046,3.198 1.302,4.8c1.302,8.173 -0.427,15.373 -7.132,20.786c-1.133,0.914 -2.531,1.579 -3.906,2.097c-2.092,0.788 -3.43,0.253 -4.71,-1.598c-1.317,-1.906 -1.929,-4.125 -1.956,-6.347c-0.043,-3.592 0.49,-7.119 2.83,-10.114c3.502,-4.483 7.622,-8.064 13.572,-9.624z" fill="#FCDACF" id="svg_155"/>
<path d="m511.621,227.118881c4.065,-0.154 8.13,-0.323 12.197,-0.451c0.98,-0.031 1.964,0.07 2.947,0.111c-1.078,3.612 -2.945,6.8 -5.242,10.06c-4.142,-2.56 -7.428,-5.713 -9.902,-9.72z" fill="#FCDACF" id="svg_156"/>
<path d="m137.585,256.566881c-0.232,1.81 -0.466,3.621 -0.694,5.431c-0.152,1.21 -0.755,1.98 -2.018,2.163c-2.277,0.33 -4.455,1.03 -6.873,0.68c-4.262,-0.616 -6.739,-3.083 -7.888,-6.96c-1.494,-5.037 -0.23,-9.754 2.481,-14.042c2.013,-3.184 5.467,-3.625 8.715,-1.513c3.204,2.084 4.885,5.144 5.587,8.784c0.345,1.788 0.465,3.616 0.69,5.457z" fill="#FCF4F2" id="svg_163"/>
<g id="BACKGROUND"/>
<g id="svg_1">
<path d="m623,300.642881c-63.833,0 -127.667,0 -191.5,0c0.109,-1.4 0.038,-2.844 0.365,-4.191c0.775,-3.196 1.715,-6.353 2.636,-9.511c0.602,-2.062 1.084,-4.195 1.979,-6.129c2.321,-5.011 3.543,-10.485 6.466,-15.235c0.126,-0.205 0.181,-0.471 0.207,-0.716c0.161,-1.56 1.197,-2.67 1.938,-3.947c1.587,-2.735 3.482,-5.229 5.48,-7.668c1.135,-1.386 2.09,-2.918 3.127,-4.385c0.878,0.766 1.792,1.494 2.627,2.303c3.598,3.486 7.44,6.677 11.787,9.17c6.411,3.678 13.171,5.353 20.6,3.493c4.237,-1.061 8.191,-2.676 12.041,-4.69c7.177,-3.755 13.324,-8.74 18.465,-14.989c0.938,-1.14 1.95,-2.218 2.932,-3.33c1.704,0.772 3.2,1.503 4.737,2.135c5.842,2.405 11.935,3.647 18.246,3.4c5.454,-0.214 10.695,-1.651 15.701,-3.762c10.855,-4.578 21.031,-10.344 30.219,-17.776c0.768,-0.622 1.628,-1.13 2.446,-1.692c2.325,-0.514 4.693,-0.884 6.966,-1.569c6.536,-1.969 12.432,-5.303 18.194,-8.877c2.296,-1.424 4.526,-2.953 6.786,-4.435c2.576,-0.065 5.17,-0.355 7.723,-0.145c4.5,0.369 8.904,1.307 12.737,3.908c2.238,1.519 3.939,3.496 4.736,6.04c-0.14,0.221 -0.167,0.323 -0.227,0.349c-1.063,0.465 -2.143,0.893 -3.194,1.383c-6.055,2.821 -10.479,7.337 -13.259,13.349c-2.424,5.241 -1.382,10.402 1.047,15.386c1.19,2.441 3.62,3.714 6.339,3.293c4.946,-0.766 8.461,-3.775 11.243,-7.64c3.751,-5.209 4.589,-11.135 3.601,-17.373c-0.335,-2.116 -0.785,-4.215 -1.232,-6.582c4.976,-0.537 9.83,-1.131 14.698,-1.569c6.83,-0.614 13.517,-1.964 19.931,-4.322c7.326,-2.694 14.531,-5.719 21.76,-8.672c7.81,-3.19 14.893,-7.747 22.164,-11.949c6.273,-3.625 11.789,-8.086 15.927,-14.114c0.129,-0.188 0.372,-0.296 0.562,-0.442c0,14.833 0,29.667 0,44.5c-2.472,0.148 -4.942,0.406 -7.415,0.423c-6.998,0.048 -13.85,1.277 -20.66,2.647c-6.182,1.243 -12.155,3.319 -18.031,5.599c-4.17,1.619 -8.171,3.69 -12.377,5.197c-6.222,2.23 -11.549,6.109 -17.404,9.01c-6.341,3.141 -11.549,7.948 -17.087,12.288c-6.142,4.813 -11.179,10.666 -16.643,16.128c-2.606,2.605 -4.496,5.696 -6.591,8.626c-3.688,5.156 -7.004,10.578 -10.453,15.904c-0.214,0.329 -0.232,0.783 -0.34,1.179z" fill="#FEDBD0" id="svg_4"/>
<path d="m623,300.642881c0.108,-0.396 0.126,-0.85 0.338,-1.178c3.449,-5.325 6.766,-10.747 10.453,-15.904c2.095,-2.93 3.985,-6.021 6.591,-8.626c5.464,-5.462 10.501,-11.315 16.643,-16.128c5.538,-4.34 10.746,-9.147 17.087,-12.288c5.855,-2.901 11.182,-6.78 17.404,-9.01c4.206,-1.507 8.207,-3.578 12.377,-5.197c5.876,-2.281 11.849,-4.356 18.031,-5.599c6.81,-1.37 13.662,-2.598 20.66,-2.647c2.472,-0.017 4.943,-0.275 7.415,-0.423c0,25.667 0,51.333 0,77c-42.332,0 -84.666,0 -126.999,0zm53.375,-36.965c1.139,-0.251 2.619,-0.443 3.985,-0.943c0.563,-0.206 1.229,-1.084 1.215,-1.641c-0.013,-0.519 -0.836,-1.415 -1.355,-1.454c-2.359,-0.18 -4.741,-0.217 -7.104,-0.089c-0.962,0.052 -1.502,0.973 -1.518,1.987c-0.016,0.99 0.656,1.517 1.505,1.71c0.952,0.218 1.942,0.265 3.272,0.43zm31.44,-5.633c-1.287,0.123 -2.369,0.146 -3.416,0.349c-0.971,0.189 -1.705,0.856 -1.602,1.897c0.107,1.09 0.798,1.832 2.005,1.764c2.388,-0.135 4.776,-0.288 7.159,-0.498c0.801,-0.071 1.394,-0.547 1.424,-1.445c0.031,-0.915 -0.549,-1.353 -1.359,-1.482c-1.467,-0.232 -2.94,-0.411 -4.211,-0.585zm18.439,-17.441c1.112,-0.215 2.701,-0.371 4.167,-0.88c0.615,-0.213 1.278,-1.134 1.369,-1.807c0.134,-0.982 -0.882,-1.442 -1.701,-1.451c-2.232,-0.024 -4.478,0.013 -6.689,0.279c-0.592,0.071 -1.399,0.978 -1.532,1.621c-0.188,0.906 0.367,1.814 1.446,2.009c0.808,0.145 1.643,0.135 2.94,0.229zm-28.051,11.011c0.01,0.05 0.02,0.1 0.03,0.149c1.375,-0.248 2.772,-0.412 4.117,-0.773c0.882,-0.237 1.453,-1.041 1.088,-1.923c-0.234,-0.566 -0.959,-1.235 -1.526,-1.297c-1.551,-0.169 -3.138,-0.088 -4.705,-0.005c-0.552,0.029 -1.29,0.196 -1.585,0.577c-0.46,0.595 -0.888,1.461 -0.802,2.145c0.056,0.438 1.022,0.872 1.655,1.091c0.52,0.178 1.149,0.036 1.728,0.036zm16.873,29.366c0.595,-0.074 1.654,-0.193 2.711,-0.339c1.004,-0.139 1.888,-0.673 1.714,-1.707c-0.109,-0.646 -0.848,-1.376 -1.491,-1.706c-1.902,-0.976 -3.9,-0.309 -5.789,0.143c-0.519,0.124 -1.159,1.292 -1.087,1.904c0.073,0.618 0.87,1.313 1.518,1.651c0.518,0.27 1.289,0.054 2.424,0.054zm-62.107,-1.702c0.992,-0.141 2.333,-0.217 3.6,-0.565c0.519,-0.142 1.203,-0.862 1.214,-1.333c0.01,-0.437 -0.739,-1.228 -1.219,-1.282c-2.214,-0.249 -4.455,-0.413 -6.676,-0.329c-0.544,0.02 -1.336,1.018 -1.5,1.692c-0.23,0.941 0.638,1.49 1.508,1.636c0.896,0.15 1.823,0.117 3.073,0.181zm56.714,10.81c-0.95,0.101 -1.623,0.107 -2.263,0.255c-0.974,0.227 -1.67,0.848 -1.699,1.888c-0.03,1.088 0.736,1.636 1.698,1.861c1.885,0.441 3.648,-0.083 5.244,-1.002c0.951,-0.548 0.863,-1.725 -0.147,-2.201c-0.951,-0.447 -2.046,-0.59 -2.833,-0.801zm26.315,-37.502c0.766,-0.263 1.911,-0.507 2.907,-1.034c1.329,-0.703 1.286,-2.119 -0.137,-2.548c-1.368,-0.413 -2.888,-0.43 -4.34,-0.418c-1.058,0.008 -1.957,0.585 -1.937,1.817c0.019,1.175 0.899,1.694 1.904,1.985c0.389,0.112 0.813,0.105 1.603,0.198zm0.345,19.21c0.952,-0.19 1.982,-0.219 2.829,-0.627c0.533,-0.258 1.055,-1.068 1.089,-1.659c0.026,-0.456 -0.629,-1.207 -1.134,-1.38c-0.902,-0.31 -1.919,-0.375 -2.888,-0.376c-2.054,-0.003 -3.157,0.781 -3.079,2.046c0.086,1.385 0.91,1.871 3.183,1.996zm-51.014,9.677c1.121,-0.151 2.125,-0.188 3.072,-0.44c1.06,-0.282 1.275,-1.309 0.548,-2.125c-0.839,-0.942 -4.41,-1.793 -5.578,-1.336c-0.85,0.332 -1.374,0.897 -1.335,1.857c0.039,0.958 0.583,1.513 1.482,1.72c0.639,0.147 1.292,0.233 1.811,0.324zm54.676,11.76c-1.539,-1.969 -3.594,-2.067 -5.636,-1.96c-0.518,0.027 -1.346,0.675 -1.414,1.13c-0.09,0.604 0.254,1.474 0.712,1.921c1.518,1.481 4.853,0.946 6.338,-1.091zm-63.279,1.341c0.587,-0.147 1.677,-0.286 2.637,-0.716c0.486,-0.218 1.047,-0.983 1.011,-1.458c-0.039,-0.507 -0.658,-1.215 -1.173,-1.388c-0.918,-0.307 -1.947,-0.334 -2.934,-0.373c-1.421,-0.057 -2.583,0.753 -2.604,1.715c-0.021,1.006 1.352,2.18 3.063,2.22zm-31.358,-4.32c-0.518,0.079 -1.188,0.107 -1.81,0.293c-0.819,0.245 -1.502,0.673 -1.465,1.719c0.041,1.148 0.804,1.672 1.744,1.718c1.049,0.052 2.16,-0.065 3.152,-0.39c0.58,-0.19 1.252,-0.858 1.4,-1.436c0.245,-0.953 -0.599,-1.458 -1.413,-1.727c-0.457,-0.149 -0.975,-0.114 -1.608,-0.177z" fill="#548099" id="svg_7"/>
<path d="m346.953,290.630881c0.806,1.415 1.73,2.778 2.39,4.258c0.832,1.868 1.448,3.832 2.157,5.754c-43.5,0 -87,0 -130.5,0c0.664,-4.841 1.467,-9.669 1.949,-14.528c0.409,-4.123 0.447,-8.283 0.649,-12.426c0.655,-0.292 1.496,-0.416 1.932,-0.903c1.935,-2.161 4.26,-3.808 6.56,-5.537c3.753,-2.82 7.508,-5.604 11.694,-7.767c0.521,-0.269 0.858,-0.895 1.28,-1.356c1.969,-0.804 3.904,-1.705 5.914,-2.389c2.802,-0.953 5.66,-1.745 8.494,-2.605c0.444,0.781 0.854,1.584 1.34,2.338c1.034,1.602 3.777,1.039 4.439,-0.312c0.597,-1.219 1.348,-2.508 0.685,-3.985c1.22,-0.01 2.44,-0.019 3.659,-0.029c0.702,1.529 1.924,2.014 3.551,1.953c1.667,-0.062 1.744,-1.442 2.288,-2.46c1.394,-0.16 2.786,-0.341 4.183,-0.474c0.906,-0.086 1.406,0.366 1.468,1.282c-0.581,0.457 -1.236,0.848 -1.725,1.388c-0.997,1.1 -1.005,2.435 -0.49,3.73c0.443,1.113 1.448,1.554 2.592,1.627c1.41,0.09 3.026,-0.987 3.526,-2.301c0.455,-1.196 0.04,-2.505 -1.157,-3.529c-0.436,-0.373 -0.922,-0.689 -1.385,-1.031c0.254,-0.166 0.511,-0.479 0.762,-0.474c1.561,0.031 3.12,0.133 4.678,0.211c1.094,0.858 2.293,1.005 3.583,0.518c0.516,0.348 0.986,0.863 1.554,1.015c2.989,0.797 5.976,1.653 9.016,2.189c2.214,0.391 4.217,1.307 6.306,1.99c2.187,0.715 4.316,1.674 6.363,2.734c2.392,1.238 4.669,2.697 6.993,4.066c3.399,2.002 6.29,4.673 9.358,7.11c-0.825,0.879 -1.67,1.742 -2.471,2.642c-2.811,3.157 -5.61,6.324 -8.407,9.493c-0.931,1.055 -1.882,2.098 -2.752,3.203c-1.282,1.629 -1.073,4.988 0.312,5.917c1.948,1.307 4.248,0.857 5.65,-1.118c0.623,-0.878 1.155,-1.829 1.845,-2.649c3.565,-4.239 7.427,-8.242 10.273,-13.047c2.732,1.785 4.457,4.533 6.55,6.923c0.331,0.378 0.315,1.06 0.46,1.601c-0.496,0.652 -1.051,1.268 -1.474,1.965c-0.803,1.321 -0.775,3.932 -0.021,4.694c1.143,1.154 3.035,1.425 4.766,0.701c0.375,-0.158 0.775,-0.257 1.163,-0.382zm-72.407,-25.96c0.019,-2.027 -1.097,-3.298 -2.884,-3.284c-1.897,0.015 -4.097,2.687 -4.097,4.975c0,1.415 1.258,2.597 2.755,2.588c2.478,-0.015 4.202,-1.761 4.226,-4.279zm25.954,-5.911c0.013,-1.961 -1.734,-3.932 -3.438,-3.88c-1.956,0.061 -3.375,1.636 -3.444,3.82c-0.054,1.74 1.356,2.824 3.628,2.788c1.744,-0.026 3.244,-1.285 3.254,-2.728zm-11.518,6.038c0.038,-2.048 -0.319,-2.983 -1.43,-3.368c-1.198,-0.415 -2.281,-0.054 -3.198,0.77c-1.084,0.975 -1.515,2.591 -1.055,3.791c0.59,1.539 1.254,2.056 2.702,2.102c1.99,0.063 2.939,-0.986 2.981,-3.295z" fill="#DAF1FF" id="svg_9"/>
<path d="m703.429,159.265881c0.502,-1.426 1.928,-1.133 2.946,-1.451c1.315,-0.41 2.746,-0.447 4.128,-0.646c-0.073,1.037 -0.284,2.089 -0.18,3.108c0.102,0.994 0.945,1.465 1.891,1.687c1.031,0.242 1.717,-0.193 2.278,-1.028c0.839,-1.249 1.542,-2.52 1.195,-4.107c2.698,-0.336 5.388,-0.789 8.096,-0.966c1.798,-0.118 3.675,0.481 5.423,0.193c4.629,-0.762 9.263,-0.294 13.889,-0.384c2.297,-0.044 4.602,0.301 6.904,0.47c0,6 0,12 0,18c-0.168,0.168 -0.379,0.31 -0.499,0.507c-3.606,5.916 -8.197,10.898 -14.1,14.566c-9.541,5.927 -19.201,11.641 -29.712,15.749c-4.875,1.905 -9.688,3.974 -14.602,5.768c-6.175,2.255 -12.495,3.985 -19.078,4.65c-6.024,0.609 -12.036,1.349 -18.086,2.036c-0.09,-0.077 -0.245,-0.163 -0.332,-0.294c-0.23,-0.345 -0.451,-0.7 -0.642,-1.069c-2.945,-5.662 -7.74,-8.766 -13.872,-9.97c-2.517,-0.494 -5.059,-0.863 -7.59,-1.289c0.122,-1.386 1.372,-1.852 2.257,-2.453c3.107,-2.111 5.83,-4.582 8.397,-7.337c2.64,-2.832 5.637,-5.351 8.608,-7.852c3.084,-2.596 6.282,-5.065 9.508,-7.485c4.242,-3.181 8.981,-5.539 13.649,-8.011c1.312,-0.695 2.5,-1.622 3.798,-2.346c2.435,-1.357 4.861,-2.746 7.38,-3.931c3.057,-1.438 5.85,-3.482 9.246,-4.157c1.207,-0.24 2.427,-0.418 3.641,-0.625c0.038,0.747 0.084,1.493 0.112,2.241c0.048,1.279 0.794,1.688 1.971,1.714c3.458,0.077 4.892,-2.151 3.376,-5.288z" fill="#FEDBD0" id="svg_10"/>
<path d="m57.51,258.254881c7.145,-2.064 14.546,-3.315 21.271,-6.71c0.294,-0.148 0.596,-0.312 0.913,-0.376c1.115,-0.226 1.846,0.461 1.457,1.539c-0.502,1.392 -1.12,2.772 -1.899,4.026c-1.746,2.811 -3.623,5.542 -5.446,8.305c-3.48,3.206 -6.708,6.758 -10.503,9.534c-5.403,3.951 -11.477,6.726 -18.266,7.474c-3.769,0.415 -7.528,1.343 -11.356,0.469c-0.543,-0.124 -1.098,-0.198 -1.702,-0.304c-2.741,5.966 -4.551,12.065 -5.48,18.43c-0.667,0 -1.333,0 -2,0c0.047,-0.996 -0.041,-2.022 0.163,-2.985c1.855,-8.751 4.84,-17.099 9.313,-24.86c2.197,-3.813 4.602,-7.509 7.008,-11.196c1.63,-2.497 3.424,-4.887 5.145,-7.325c0.707,-0.793 1.414,-1.587 1.826,-2.049c-1.337,-4.444 -2.733,-8.452 -3.722,-12.558c-0.906,-3.763 -0.302,-7.588 0.281,-11.383c0.427,-2.786 0.691,-5.602 0.898,-8.414c0.539,-7.303 1.03,-14.61 1.491,-21.919c0.141,-2.242 0.072,-4.498 0.198,-6.741c0.083,-1.468 -0.072,-3.421 1.695,-3.792c1.799,-0.378 2.727,1.386 3.382,2.777c1.878,3.985 3.742,7.986 4.607,12.361c0.5,2.527 1.048,5.048 1.682,7.544c1.66,6.54 1.353,13.035 -0.341,19.504c-1.01,3.857 -2.083,7.696 -3.115,11.547c-0.081,0.302 -0.059,0.632 -0.138,1.602c4.488,-4.461 7.787,-9.186 11.478,-13.553c3.756,-4.445 7.3,-9.081 10.746,-13.773c3.441,-4.686 6.087,-9.859 8.756,-15.053c-1.95,-0.825 -3.721,-1.493 -5.418,-2.314c-1.995,-0.966 -3.474,-2.515 -4.659,-4.396c-2.918,-4.634 -4.822,-9.683 -6.084,-14.97c-1.099,-4.607 -2.018,-9.257 -2.983,-13.895c-0.37,-1.781 0.934,-2.944 2.664,-2.336c0.675,0.237 1.307,0.735 1.828,1.246c5.293,5.196 10.216,10.715 14.295,16.93c0.951,1.448 1.61,3.09 2.377,4.656c0.715,1.462 1.396,2.941 2.091,4.412c0.184,-0.025 0.369,-0.049 0.553,-0.074c0.57,-2.137 1.031,-4.311 1.741,-6.4c0.686,-2.019 0.513,-3.751 -0.461,-5.692c-1.933,-3.85 -2.263,-8.14 -2.788,-12.352c-0.267,-2.142 -0.713,-4.277 -0.791,-6.425c-0.243,-6.719 2.212,-12.722 5.395,-18.432c1.78,-3.192 3.947,-6.169 5.951,-9.235c0.226,-0.346 0.484,-0.688 0.789,-0.962c1.019,-0.913 1.987,-0.691 2.548,0.593c0.197,0.453 0.33,0.948 0.404,1.438c0.771,5.099 1.548,10.196 1.283,15.386c-0.242,4.729 -0.409,9.461 -0.621,14.191c-0.263,5.86 -1.892,11.295 -5.25,16.133c-0.69,0.994 -1.642,1.891 -2.657,2.548c-0.904,0.586 -1.382,1.11 -1.026,2.166c0.173,0.034 0.378,0.141 0.461,0.081c4.238,-3.071 9.005,-4.968 13.999,-6.39c3.119,-0.888 6.217,-1.852 9.316,-2.809c0.709,-0.219 1.533,-0.754 1.974,0.12c0.211,0.418 -0.073,1.317 -0.429,1.756c-3.838,4.724 -7.825,9.269 -13.147,12.472c-4.119,2.479 -8.495,3.238 -13.181,2.755c-0.401,-0.041 -0.801,-0.094 -1.403,-0.165c-0.513,1.27 -1.061,2.461 -1.48,3.695c-1.126,3.313 -2.51,6.49 -4.453,9.422c-0.589,0.889 -0.936,1.94 -1.393,2.917c-0.092,0.177 -0.184,0.354 -0.276,0.531c0.237,-0.025 0.475,-0.049 0.712,-0.074c8.518,-4.686 17.852,-5.489 27.302,-5.726c4.238,-0.106 8.496,0.056 12.66,-0.997c0.32,-0.081 0.688,-0.208 0.977,-0.122c0.431,0.129 0.934,0.345 1.165,0.687c0.135,0.199 -0.127,0.769 -0.346,1.076c-0.233,0.327 -0.62,0.546 -0.949,0.8c-4.471,3.456 -8.851,7.037 -13.446,10.319c-3.093,2.209 -6.334,4.341 -9.788,5.883c-4.479,1.999 -9.254,2.422 -14.099,0.841c-3.231,-1.054 -6.577,-1.775 -9.11,-4.412c-5.55,7.005 -10.925,13.789 -16.3,20.573c0.1,0.131 0.201,0.262 0.301,0.394c1.158,-0.542 2.28,-1.184 3.48,-1.603c2.105,-0.736 4.22,-1.533 6.399,-1.952c4.669,-0.897 9.384,-1.167 14.146,-0.602c2.304,0.273 4.653,0.157 6.981,0.233c0.495,0.016 1.018,0.013 1.476,0.172c0.931,0.323 1.159,1.218 0.56,1.997c-0.15,0.194 -0.348,0.364 -0.553,0.501c-5.39,3.602 -10.583,7.572 -16.826,9.66c-4.521,1.513 -9.105,2.594 -13.959,1.56c-2.984,-0.636 -6.028,-0.992 -9.407,-1.532c-1.229,1.622 -2.828,3.432 -4.06,5.465c-0.787,1.297 -1.522,2.517 -2.777,3.402c-0.313,0.22 -0.452,0.686 -0.672,1.038c-2.79,3.854 -5.6,7.693 -8.352,11.574c-0.505,0.712 -0.774,1.592 -1.151,2.396c-0.148,0.187 -0.298,0.372 -0.446,0.558c-0.049,0.137 -0.098,0.273 -0.148,0.409c0.085,-0.101 0.169,-0.202 0.254,-0.303c0.175,-0.159 0.349,-0.318 0.524,-0.476c0.359,-0.195 0.757,-0.343 1.072,-0.594c4.498,-3.578 9.142,-6.901 14.687,-8.726c0.769,-0.253 1.457,-0.754 2.182,-1.141zm-3.085,-15.668c-0.545,0.543 -0.931,1.13 -0.546,2.46c0.526,-0.893 0.821,-1.396 1.117,-1.898c-0.19,-0.188 -0.381,-0.375 -0.571,-0.562z" fill="#56819B" id="svg_11"/>
<path d="m203.5,300.642881c-28.167,0 -56.333,0 -84.5,0c1.579,-2.025 3.197,-4.021 4.731,-6.079c6.246,-8.378 11.41,-17.326 14.302,-27.453c0.29,-1.015 0.785,-1.708 1.872,-2.113c1.625,-0.605 3.261,-1.3 4.707,-2.241c2.703,-1.757 5.273,-3.719 7.899,-5.595c0.724,0.841 1.492,0.323 2.277,0.105c1.509,-0.419 3.015,-0.961 4.557,-1.123c1.518,-0.159 2.939,-0.342 4.212,-1.261c0.46,-0.332 1.027,-0.601 1.581,-0.702c3.082,-0.565 6.171,-1.099 9.267,-1.587c3.507,-0.553 7.013,-1.126 10.537,-1.545c4.593,-0.547 9.198,-1.015 13.808,-1.394c3.384,-0.278 6.784,-0.345 10.175,-0.546c0.372,-0.022 0.727,-0.309 1.091,-0.473c0.773,0.86 1.621,1.664 2.303,2.59c1.619,2.199 3.404,4.325 4.683,6.715c3.164,5.915 4.317,12.338 4.11,19.02c-1.822,1.284 -3.545,2.656 -4.742,4.6c-0.692,1.125 -1.486,2.192 -2.282,3.248c-1.578,2.094 -3.217,4.141 -4.778,6.247c-1.37,1.847 -2.75,3.695 -3.969,5.642c-0.765,1.221 -1.238,2.624 -1.841,3.945z" fill="#B2CDDD" id="svg_12"/>
<path d="m0,167.642881c1.953,0.858 4.023,1.524 5.837,2.613c4.339,2.606 8.422,5.588 12.183,9.001c7.607,6.902 12.849,15.19 15.549,25.179c1.135,4.2 1.898,8.407 2.177,12.703c0.491,7.553 -0.901,14.816 -3.643,21.862c-1.81,4.652 -4.023,9.07 -6.872,13.17c-0.178,0.257 -0.213,0.613 -0.314,0.923c-0.706,0.697 -1.407,1.399 -2.12,2.089c-1.599,1.548 -3.107,3.208 -4.825,4.612c-4.805,3.927 -9.685,7.762 -14.564,11.598c-1.008,0.792 -1.893,1.912 -3.409,1.75c0.001,-35.167 0.001,-70.333 0.001,-105.5z" fill="#54809A" id="svg_14"/>
<path d="m26.5,300.642881c0.929,-6.365 2.74,-12.464 5.48,-18.43c0.603,0.106 1.158,0.18 1.702,0.304c3.828,0.874 7.586,-0.054 11.356,-0.469c6.789,-0.748 12.863,-3.524 18.266,-7.474c3.796,-2.775 7.023,-6.328 10.503,-9.534c3.102,2.025 6.632,2.88 10.186,3.581c4.458,0.88 8.946,1.606 13.421,2.397c0.163,0.029 0.331,0.078 0.493,0.066c5.706,-0.412 11.501,0.316 17.092,-1.612c2.083,-0.719 4.354,-0.881 6.519,-1.383c0.977,-0.227 2.248,-0.094 2.553,-1.51c3.712,1.091 7.464,0.778 11.605,0.016c-0.403,1.373 -0.662,2.386 -0.997,3.374c-3.081,9.089 -8.229,17.02 -13.851,24.685c-1.456,1.986 -2.886,3.991 -4.328,5.988c-30,0.001 -60,0.001 -90,0.001z" fill="#B2CDDD" id="svg_15"/>
<path d="m453.697,248.860881c-1.036,1.466 -1.992,2.999 -3.127,4.385c-1.997,2.439 -3.893,4.933 -5.48,7.668c-0.741,1.277 -1.777,2.387 -1.938,3.947c-0.025,0.244 -0.081,0.511 -0.207,0.716c-2.923,4.751 -4.145,10.224 -6.466,15.235c-0.896,1.934 -1.377,4.067 -1.979,6.129c-0.922,3.158 -1.861,6.316 -2.636,9.511c-0.327,1.347 -0.255,2.791 -0.365,4.191c-8.333,0 -16.667,0 -25,0c-0.064,-0.492 -0.127,-0.984 -0.191,-1.476c-0.786,-6.001 -1.833,-11.982 -2.289,-18.008c-0.5,-6.618 -0.296,-13.268 0.94,-19.837c1.29,-6.857 3.538,-13.302 7.994,-18.814c1.804,-2.231 3.905,-4.056 6.617,-5.126c7.006,-2.763 13.85,-2.131 20.494,1.109c5.215,2.544 9.46,6.425 13.633,10.37z" fill="#FEF6F3" id="svg_18"/>
<path d="m453.697,248.860881c-4.173,-3.945 -8.418,-7.826 -13.632,-10.369c-6.644,-3.241 -13.488,-3.872 -20.494,-1.109c-2.712,1.069 -4.814,2.894 -6.617,5.126c-4.456,5.512 -6.704,11.957 -7.994,18.814c-1.236,6.569 -1.44,13.219 -0.94,19.837c0.456,6.026 1.503,12.007 2.289,18.008c0.064,0.492 0.128,0.984 0.191,1.476c-1,0 -2,0 -3,0c-0.048,-0.899 -0.047,-1.804 -0.152,-2.696c-0.636,-5.436 -1.441,-10.857 -1.901,-16.307c-0.335,-3.965 -0.414,-7.975 -0.272,-11.952c0.276,-7.714 1.882,-15.169 5.327,-22.127c2.157,-4.356 4.886,-8.252 9.027,-11.079c4.653,-3.177 9.791,-4.007 15.208,-3.599c7.091,0.533 12.977,3.859 18.324,8.305c2.236,1.859 4.459,3.733 6.689,5.599c3.79,3.227 7.337,6.807 11.421,9.605c7.432,5.091 15.641,6.85 24.394,3.67c4.21,-1.529 8.286,-3.37 12.08,-5.845c5.415,-3.533 10.19,-7.729 14.251,-12.749c0.603,-0.745 1.155,-1.531 1.799,-2.388c-4.865,-3.29 -8.586,-7.267 -11.563,-11.948c-1.977,-4.036 -3.754,-8.144 -4.246,-12.674c-0.618,-5.684 0.519,-10.88 4.306,-15.343c7.45,-8.777 17.131,-4.118 20.673,1.989c3.019,5.207 4.006,10.856 2.952,16.788c-0.522,2.935 -1.291,5.827 -1.948,8.738c-1.193,2.637 -2.33,5.303 -3.604,7.9c-0.639,1.302 -1.527,2.482 -2.352,3.795c1.172,0.621 2.086,1.221 3.08,1.615c10.465,4.143 20.984,4.634 31.595,0.488c5.979,-2.336 11.681,-5.231 17.151,-8.551c3.608,-2.19 6.999,-4.739 10.487,-7.127c2.397,-1.768 4.815,-3.507 7.186,-5.31c7.254,-5.519 15.14,-9.814 23.882,-12.49c3.984,-1.219 8.052,-1.862 12.21,-1.993c0.661,-0.021 1.321,-0.104 1.981,-0.157c2.531,0.427 5.073,0.795 7.59,1.29c6.133,1.204 10.928,4.308 13.872,9.97c0.191,0.368 0.411,0.723 0.642,1.069c0.087,0.131 0.242,0.217 0.332,0.294c6.05,-0.686 12.062,-1.426 18.086,-2.036c6.583,-0.666 12.902,-2.396 19.078,-4.65c4.914,-1.794 9.728,-3.863 14.602,-5.768c10.511,-4.108 20.171,-9.822 29.712,-15.749c5.903,-3.667 10.494,-8.65 14.1,-14.566c0.12,-0.197 0.331,-0.339 0.499,-0.507c0,1.667 0,3.333 0,5c-0.19,0.146 -0.434,0.254 -0.562,0.442c-4.137,6.029 -9.654,10.489 -15.927,14.114c-7.271,4.202 -14.354,8.758 -22.164,11.949c-7.229,2.953 -14.434,5.979 -21.76,8.672c-6.414,2.358 -13.101,3.708 -19.931,4.322c-4.868,0.438 -9.722,1.032 -14.698,1.569c0.447,2.367 0.896,4.466 1.232,6.582c0.989,6.238 0.15,12.164 -3.601,17.373c-2.782,3.864 -6.297,6.874 -11.243,7.64c-2.72,0.421 -5.149,-0.852 -6.339,-3.293c-2.429,-4.984 -3.471,-10.145 -1.047,-15.386c2.78,-6.012 7.204,-10.527 13.259,-13.349c1.051,-0.49 2.131,-0.918 3.194,-1.383c0.06,-0.026 0.087,-0.129 0.227,-0.349c-0.797,-2.545 -2.498,-4.521 -4.736,-6.04c-3.833,-2.602 -8.237,-3.539 -12.737,-3.908c-2.553,-0.209 -5.147,0.08 -7.722,0.145c-2.839,0.556 -5.759,0.858 -8.5,1.723c-5.473,1.727 -10.671,4.173 -15.496,7.29c-2.762,1.783 -5.307,3.901 -7.95,5.869c-0.817,0.561 -1.677,1.07 -2.446,1.692c-9.188,7.432 -19.365,13.198 -30.219,17.776c-5.006,2.111 -10.247,3.548 -15.701,3.762c-6.311,0.247 -12.404,-0.995 -18.246,-3.4c-1.536,-0.632 -3.032,-1.364 -4.737,-2.135c-0.982,1.112 -1.995,2.19 -2.932,3.33c-5.141,6.249 -11.288,11.234 -18.465,14.989c-3.85,2.014 -7.804,3.629 -12.041,4.69c-7.43,1.86 -14.189,0.184 -20.6,-3.493c-4.347,-2.493 -8.189,-5.684 -11.787,-9.17c-0.834,-0.816 -1.748,-1.545 -2.626,-2.31zm73.068,-22.082c1.719,-5.217 2.952,-10.54 2.417,-16.06c-0.418,-4.322 -1.932,-8.262 -5.474,-11.091c-3.886,-3.104 -8.374,-3.952 -12.441,0.361c-3.003,3.185 -4.691,7.101 -4.647,11.502c0.057,5.679 1.696,10.943 5.001,15.629c2.474,4.007 5.76,7.16 9.901,9.719c2.297,-3.261 4.164,-6.449 5.243,-10.06zm125.196,-5.674c-5.95,1.56 -10.071,5.141 -13.571,9.623c-2.34,2.996 -2.873,6.522 -2.83,10.114c0.026,2.222 0.639,4.441 1.956,6.347c1.279,1.852 2.618,2.387 4.71,1.598c1.375,-0.518 2.773,-1.183 3.906,-2.097c6.705,-5.413 8.435,-12.613 7.132,-20.786c-0.256,-1.601 -0.846,-3.15 -1.303,-4.799z" fill="#5D859D" id="svg_20"/>
<path d="m0,273.142881c1.516,0.161 2.401,-0.958 3.409,-1.75c4.879,-3.836 9.759,-7.671 14.564,-11.598c1.717,-1.404 3.226,-3.064 4.825,-4.612c0.713,-0.69 1.414,-1.392 2.12,-2.089c0.999,0.956 2.174,0.507 3.297,0.397c2.798,-0.273 5.591,-0.608 8.392,-0.847c3.306,-0.282 6.367,0.969 9.522,1.631c-1.72,2.438 -3.515,4.828 -5.144,7.325c-2.406,3.687 -4.811,7.384 -7.008,11.196c-4.473,7.761 -7.457,16.109 -9.313,24.86c-0.204,0.963 -0.116,1.988 -0.163,2.985c-8.167,0 -16.333,0 -24.5,0c-0.001,-9.165 -0.001,-18.331 -0.001,-27.498z" fill="#B2CDDD" id="svg_24"/>
<path d="m116.5,300.642881c1.442,-1.996 2.872,-4.002 4.328,-5.988c5.622,-7.665 10.77,-15.596 13.851,-24.685c0.335,-0.988 0.594,-2.002 0.997,-3.374c-4.141,0.762 -7.892,1.075 -11.605,-0.016c-2.444,-2.027 -4.795,-4.112 -5.824,-7.288c-2.189,-6.757 -0.875,-12.83 3.686,-18.161c2.091,-2.444 5.436,-2.988 8.48,-1.804c4.269,1.661 6.988,4.729 8.105,9.135c0.943,3.719 1.461,7.501 0.999,11.362c-0.087,0.726 -0.013,1.471 -0.013,2.42c0.622,-0.123 1.191,-0.128 1.66,-0.349c1.127,-0.53 2.259,-1.079 3.295,-1.764c5.63,-3.723 11.032,-7.753 15.879,-12.467c8.534,-8.302 18.932,-11.766 30.552,-10.834c8.313,0.666 15.79,3.901 21.214,10.745c0.508,0.641 1.185,1.147 1.784,1.715c1.658,2.364 3.474,4.635 4.939,7.113c3.137,5.308 4.259,11.225 4.77,17.286c-0.202,4.143 -0.24,8.303 -0.649,12.426c-0.482,4.859 -1.285,9.687 -1.949,14.528c-0.667,0 -1.333,0 -2,0c0.064,-0.902 0.077,-1.811 0.199,-2.704c0.954,-6.964 1.934,-13.924 1.911,-20.977c0.208,-6.682 -0.946,-13.105 -4.11,-19.02c-1.279,-2.39 -3.064,-4.516 -4.683,-6.715c-0.682,-0.926 -1.53,-1.731 -2.303,-2.59c-1.733,-2.164 -3.651,-4.181 -6.227,-5.273c-3.279,-1.39 -6.602,-2.749 -10.019,-3.724c-4.067,-1.161 -8.247,-0.643 -12.392,-0.074c-6.704,0.92 -12.55,3.703 -17.688,8.031c-3.75,3.16 -7.454,6.375 -11.178,9.566c-2.626,1.876 -5.196,3.838 -7.899,5.595c-1.447,0.941 -3.082,1.635 -4.707,2.241c-1.087,0.405 -1.582,1.099 -1.872,2.113c-2.892,10.127 -8.056,19.075 -14.302,27.453c-1.535,2.058 -3.152,4.055 -4.731,6.079c-0.831,-0.001 -1.665,-0.001 -2.498,-0.001zm21.085,-44.076c-0.226,-1.841 -0.346,-3.67 -0.69,-5.455c-0.702,-3.64 -2.383,-6.701 -5.587,-8.784c-3.248,-2.112 -6.703,-1.67 -8.715,1.513c-2.711,4.287 -3.974,9.005 -2.481,14.042c1.149,3.876 3.627,6.344 7.888,6.96c2.418,0.349 4.596,-0.35 6.873,-0.68c1.263,-0.183 1.866,-0.953 2.018,-2.163c0.228,-1.812 0.463,-3.622 0.694,-5.433z" fill="#5E879F" id="svg_33"/>
<path d="m221.11,276.961881c0.023,7.053 -0.957,14.013 -1.911,20.977c-0.123,0.894 -0.136,1.803 -0.199,2.704c-5.167,0 -10.333,0 -15.5,0c0.603,-1.321 1.076,-2.724 1.839,-3.944c1.218,-1.946 2.599,-3.795 3.969,-5.642c1.561,-2.105 3.2,-4.153 4.778,-6.247c0.796,-1.057 1.59,-2.123 2.282,-3.248c1.197,-1.945 2.92,-3.316 4.742,-4.6z" fill="#DAF1FF" id="svg_34"/>
<path d="m508.133,227.129881c2.977,4.682 6.698,8.658 11.563,11.948c-0.643,0.857 -1.196,1.643 -1.799,2.388c-4.06,5.019 -8.836,9.216 -14.251,12.749c-3.794,2.475 -7.869,4.315 -12.08,5.845c-8.754,3.18 -16.962,1.421 -24.394,-3.67c-4.084,-2.797 -7.631,-6.378 -11.421,-9.605c0.51,-0.232 1.208,-0.327 1.499,-0.721c1.406,-1.904 3.562,-2.659 5.458,-3.826c0.553,-0.341 0.885,-1.041 1.319,-1.576c2.188,-1.133 4.354,-2.31 6.567,-3.391c5.425,-2.651 10.837,-5.319 16.666,-7.044c1.654,-0.49 3.333,-0.605 4.974,-0.974c5.244,-1.179 10.582,-1.552 15.899,-2.123z" fill="#FEDBD0" id="svg_39"/>
<path d="m586.228,224.746881c-3.488,2.388 -6.878,4.937 -10.487,7.127c-5.47,3.32 -11.172,6.215 -17.151,8.551c-10.612,4.146 -21.13,3.656 -31.595,-0.488c-0.995,-0.394 -1.908,-0.993 -3.08,-1.615c0.824,-1.313 1.713,-2.492 2.352,-3.795c1.274,-2.598 2.411,-5.263 3.604,-7.9c5.575,0.006 11.151,0.061 16.726,-0.003c2.524,-0.029 5.044,-0.372 7.568,-0.427c1.561,-0.034 3.128,0.21 4.693,0.321c0.579,0.041 1.161,0.112 1.738,0.083c2.645,-0.133 5.286,-0.381 7.931,-0.43c5.194,-0.097 10.349,-0.52 15.467,-1.418c0.723,-0.127 1.488,-0.01 2.234,-0.006z" fill="#FEDAD0" id="svg_40"/>
<path d="m595.508,185.574881c1.815,-0.428 3.265,-0.768 4.714,-1.114c1.426,-0.341 2.629,-0.045 3.518,1.197c0.932,1.301 0.614,2.648 -0.249,3.691c-1.258,1.521 -2.76,2.843 -4.187,4.221c-0.475,0.459 -1.03,0.837 -1.562,1.234c-0.678,0.505 -1.005,1.152 -0.635,1.948c0.77,1.659 0.227,2.937 -1.15,3.892c-1.168,0.81 -2.502,0.376 -4.104,-1.156c-0.049,-0.047 -0.145,-0.046 -0.414,-0.123c-1.114,0.772 -2.349,1.596 -3.549,2.469c-1.042,0.758 -2.035,0.944 -3.203,0.204c-1.139,-0.721 -1.505,-1.711 -1.397,-2.964c0.128,-1.487 0.271,-2.972 0.428,-4.683c-0.666,-0.273 -1.329,-0.554 -1.999,-0.816c-0.692,-0.271 -1.439,-0.442 -2.076,-0.806c-0.871,-0.497 -1.611,-2.212 -1.29,-3.033c0.402,-1.03 1.054,-1.877 2.306,-2.007c1.484,-0.154 2.961,-0.413 4.449,-0.496c1.31,-0.073 2.229,-0.56 2.663,-1.83c0.614,-1.798 1.3,-3.576 1.818,-5.402c0.548,-1.934 1.235,-2.706 2.722,-2.679c1.553,0.028 2.468,0.811 2.643,2.367c0.21,1.888 0.358,3.784 0.554,5.886z" fill="#57829B" id="svg_41"/>
<path d="m715.687,156.829881c0.347,1.587 -0.356,2.857 -1.195,4.107c-0.56,0.834 -1.247,1.27 -2.278,1.028c-0.946,-0.222 -1.79,-0.693 -1.891,-1.687c-0.104,-1.019 0.107,-2.071 0.18,-3.108c0.053,-0.063 0.104,-0.129 0.16,-0.19c2.162,-2.296 2.731,-2.313 5.024,-0.15z" fill="#648CA3" id="svg_57"/>
<path d="m706.431,153.888881c-1.644,0.036 -3.161,-1.439 -3.192,-3.104c-0.028,-1.499 1.198,-2.548 3.001,-2.567c1.618,-0.017 3.153,1.284 3.199,2.789c0.058,1.866 -1.115,2.593 -3.008,2.882z" fill="#6F92A7" id="svg_58"/>
<path d="m703.429,159.265881c1.516,3.137 0.082,5.365 -3.374,5.289c-1.177,-0.026 -1.923,-0.435 -1.971,-1.714c-0.028,-0.747 -0.074,-1.493 -0.112,-2.241c1.107,-1.124 2.482,-1.565 4.025,-1.576c0.175,0.01 0.35,0.019 0.525,0.029c0.302,0.072 0.604,0.142 0.907,0.213z" fill="#6F92A7" id="svg_61"/>
<path d="m702.521,159.053881c-0.175,-0.009 -0.35,-0.019 -0.525,-0.029c0.175,0.009 0.35,0.019 0.525,0.029z" fill="#FEDBD0" id="svg_63"/>
<path d="m223.597,273.687881c-0.512,-6.061 -1.633,-11.978 -4.77,-17.286c-1.465,-2.479 -3.282,-4.749 -4.939,-7.113c4.654,-0.328 9.281,-0.385 13.788,1.146c0.932,0.317 1.925,0.51 2.906,0.615c4.351,0.469 7.965,2.621 11.503,4.953c1.017,0.67 1.987,1.413 2.979,2.123c-0.421,0.461 -0.758,1.086 -1.279,1.356c-4.186,2.163 -7.941,4.946 -11.694,7.767c-2.3,1.728 -4.625,3.375 -6.56,5.537c-0.438,0.486 -1.279,0.611 -1.934,0.902z" fill="#B2CDDD" id="svg_64"/>
<path d="m335.508,275.129881c-2.846,4.805 -6.708,8.808 -10.273,13.047c-0.69,0.82 -1.222,1.771 -1.845,2.649c-1.402,1.974 -3.702,2.424 -5.65,1.118c-1.385,-0.929 -1.593,-4.288 -0.312,-5.917c0.87,-1.105 1.82,-2.147 2.752,-3.203c2.797,-3.169 5.596,-6.336 8.407,-9.493c0.802,-0.9 1.646,-1.763 2.471,-2.642c0.73,-0.128 1.455,-0.312 2.191,-0.373c1.829,-0.152 2.838,0.825 2.697,2.636c-0.056,0.733 -0.286,1.452 -0.438,2.178z" fill="#B4CFDE" id="svg_66"/>
<path d="m365.213,292.639881c-2.923,0.011 -4.061,-1.133 -3.662,-3.836c0.188,-1.269 0.596,-2.692 1.385,-3.648c2.256,-2.732 4.501,-5.508 7.436,-7.601c2.437,-1.738 4.98,-1.037 5.309,1.618c0.102,0.82 -0.158,1.866 -0.631,2.545c-2.263,3.244 -4.637,6.413 -7.017,9.575c-0.726,0.965 -1.689,1.614 -2.82,1.347z" fill="#B7D0DE" id="svg_68"/>
<path d="m345.5,268.101881c-2.654,0.057 -4.406,-1.231 -4.777,-3.208c-0.12,-0.641 0.009,-1.577 0.414,-2.029c2.596,-2.899 5.285,-5.716 7.986,-8.519c0.445,-0.463 1.105,-0.714 1.651,-1.085c1.131,-0.771 2.18,-0.518 3.214,0.206c1.045,0.731 1.135,1.807 0.959,2.898c-0.359,2.215 -1.79,3.89 -3.09,5.585c-1.255,1.636 -2.622,3.204 -4.082,4.659c-0.761,0.758 -1.828,1.209 -2.275,1.493z" fill="#B7D0DE" id="svg_69"/>
<path d="m362.953,268.123881c-2.726,-0.059 -4.533,-2.69 -3.744,-5.101c0.43,-1.314 1.096,-2.661 2.005,-3.683c1.368,-1.538 2.89,-3.059 4.636,-4.101c1.254,-0.749 3.001,-1.022 4.494,-0.932c1.821,0.11 2.8,2.638 1.747,4.331c-0.991,1.594 -1.889,3.258 -3.578,4.394c-1.045,0.703 -1.697,2 -2.499,3.05c-0.892,1.168 -1.963,2 -3.061,2.042z" fill="#B7D0DE" id="svg_70"/>
<path d="m346.953,290.630881c-0.389,0.126 -0.788,0.224 -1.164,0.382c-1.732,0.724 -3.623,0.453 -4.766,-0.701c-0.755,-0.762 -0.782,-3.373 0.021,-4.694c0.423,-0.696 0.978,-1.312 1.474,-1.965c0.473,-0.289 1.11,-0.468 1.391,-0.887c1.734,-2.582 4.36,-3.985 6.856,-5.494c2.881,1.421 3.464,2.328 2.74,4.45c-0.288,0.844 -0.712,1.688 -1.258,2.389c-1.717,2.21 -3.522,4.352 -5.294,6.52z" fill="#B7D0DE" id="svg_71"/>
<path d="m57.51,258.254881c-0.725,0.387 -1.413,0.888 -2.182,1.141c-5.545,1.826 -10.189,5.148 -14.687,8.726c-0.315,0.251 -0.713,0.398 -1.073,0.593c-0.061,-0.064 -0.122,-0.126 -0.184,-0.188c0.378,-0.802 0.647,-1.682 1.152,-2.394c2.752,-3.881 5.562,-7.72 8.352,-11.574c0.196,1.12 1.068,0.99 1.89,1.103c2.438,0.336 4.816,0.879 6.732,2.593z" fill="#B2CDDD" id="svg_72"/>
<path d="m270.464,237.841881c-0.074,1.702 -1.733,3.845 -3.338,4.195c-1.004,0.219 -1.98,0.172 -2.694,-0.786c-0.887,-1.19 -1.42,-2.497 -0.845,-3.92c0.328,-0.812 0.93,-1.582 1.586,-2.177c0.994,-0.901 2.15,-1.678 3.602,-0.953c1.113,0.558 1.759,2.025 1.689,3.641z" fill="#5E879F" id="svg_77"/>
<path d="m286.407,233.645881c-0.038,2.102 -1.875,3.799 -4.097,3.782c-1.9,-0.014 -3.265,-1.56 -3.191,-3.616c0.07,-1.971 2.163,-4.021 4.041,-3.957c1.579,0.054 3.278,2.038 3.247,3.791z" fill="#5D859D" id="svg_79"/>
<path d="m265.936,251.172881c0.663,1.477 -0.088,2.766 -0.685,3.985c-0.662,1.351 -3.405,1.914 -4.439,0.312c-0.486,-0.754 -0.896,-1.557 -1.34,-2.338c0.027,-0.161 0.077,-0.321 0.078,-0.482c0.005,-1.561 0.793,-2.748 2.171,-3.216c1.382,-0.47 2.73,0.014 3.716,1.205c0.155,0.188 0.332,0.357 0.499,0.534z" fill="#5E879F" id="svg_81"/>
<path d="m298.765,241.316881c1.714,-0.007 3.078,1.376 3.141,3.186c0.065,1.862 -1.575,3.436 -3.519,3.378c-1.716,-0.052 -3.134,-1.535 -3.102,-3.246c0.029,-1.599 1.824,-3.311 3.48,-3.318z" fill="#5E879F" id="svg_82"/>
<path d="m282.445,251.329881c0.463,0.342 0.949,0.658 1.385,1.031c1.197,1.024 1.613,2.333 1.157,3.529c-0.5,1.314 -2.117,2.391 -3.526,2.301c-1.144,-0.073 -2.149,-0.514 -2.592,-1.627c-0.515,-1.295 -0.507,-2.629 0.49,-3.73c0.49,-0.54 1.145,-0.931 1.725,-1.388c0.454,-0.038 0.908,-0.077 1.361,-0.116z" fill="#5E879F" id="svg_83"/>
<path d="m275.434,250.637881c-0.544,1.018 -0.621,2.398 -2.288,2.46c-1.627,0.06 -2.849,-0.425 -3.551,-1.953c-0.009,-0.574 -0.08,-1.155 -0.016,-1.721c0.219,-1.933 1.62,-3.182 3.424,-3.108c1.562,0.064 2.446,1.248 2.477,3.336c0.005,0.328 -0.03,0.657 -0.046,0.986z" fill="#648CA3" id="svg_85"/>
<path d="m291.468,251.583881c-1.29,0.487 -2.489,0.34 -3.583,-0.518c-0.97,-2.772 -0.614,-3.836 1.427,-5.153c1.211,-0.782 2.179,-0.419 3.161,0.28c1.001,0.713 1.325,1.786 0.845,2.838c-0.429,0.938 -1.219,1.709 -1.85,2.553z" fill="#648CA3" id="svg_86"/>
<path d="m281.534,241.295881c1.544,-0.012 2.727,1.275 2.695,2.933c-0.03,1.558 -1.463,2.836 -3.168,2.825c-1.485,-0.01 -2.853,-1.174 -2.926,-2.491c-0.089,-1.609 1.621,-3.253 3.399,-3.267z" fill="#648CA3" id="svg_87"/>
<path d="m293.736,231.754881c1.644,0.208 2.646,1.505 2.404,3.111c-0.277,1.838 -1.463,3.036 -2.797,2.827c-1.772,-0.278 -3.009,-1.846 -2.752,-3.487c0.254,-1.615 1.58,-2.649 3.145,-2.451z" fill="#648CA3" id="svg_88"/>
<path d="m593.498,223.123881c2.643,-1.967 5.188,-4.085 7.95,-5.869c4.826,-3.116 10.023,-5.563 15.496,-7.29c2.742,-0.865 5.661,-1.167 8.5,-1.723c-2.26,1.482 -4.49,3.011 -6.786,4.435c-5.762,3.575 -11.658,6.908 -18.194,8.877c-2.273,0.686 -4.641,1.056 -6.966,1.57z" fill="#FDFDFE" id="svg_122"/>
<path d="m676.375,263.677881c-1.33,-0.165 -2.321,-0.213 -3.273,-0.429c-0.848,-0.193 -1.521,-0.721 -1.505,-1.71c0.016,-1.015 0.557,-1.935 1.518,-1.987c2.362,-0.128 4.745,-0.09 7.104,0.089c0.519,0.039 1.342,0.936 1.355,1.454c0.014,0.556 -0.651,1.434 -1.215,1.641c-1.365,0.5 -2.844,0.691 -3.984,0.942z" fill="#EDD1CA" id="svg_131"/>
<path d="m707.815,258.044881c1.271,0.174 2.745,0.353 4.21,0.585c0.81,0.129 1.39,0.567 1.359,1.482c-0.03,0.897 -0.622,1.374 -1.424,1.445c-2.382,0.211 -4.77,0.363 -7.159,0.498c-1.207,0.068 -1.898,-0.673 -2.005,-1.764c-0.103,-1.041 0.632,-1.708 1.602,-1.897c1.047,-0.203 2.13,-0.226 3.417,-0.349z" fill="#EDD1CA" id="svg_132"/>
<path d="m726.254,240.603881c-1.297,-0.094 -2.132,-0.084 -2.94,-0.23c-1.079,-0.195 -1.634,-1.103 -1.446,-2.009c0.133,-0.642 0.94,-1.549 1.532,-1.621c2.211,-0.265 4.457,-0.303 6.689,-0.279c0.819,0.009 1.835,0.469 1.701,1.451c-0.092,0.674 -0.755,1.594 -1.369,1.807c-1.466,0.51 -3.055,0.666 -4.167,0.881z" fill="#EDD1CA" id="svg_133"/>
<path d="m698.203,251.614881c-0.58,0 -1.209,0.142 -1.727,-0.036c-0.633,-0.219 -1.599,-0.652 -1.655,-1.091c-0.087,-0.684 0.341,-1.55 0.802,-2.145c0.295,-0.381 1.033,-0.548 1.585,-0.577c1.567,-0.083 3.154,-0.164 4.705,0.005c0.566,0.062 1.292,0.731 1.526,1.297c0.365,0.882 -0.206,1.686 -1.088,1.923c-1.345,0.361 -2.742,0.525 -4.117,0.773c-0.01,-0.05 -0.02,-0.1 -0.031,-0.149z" fill="#EDD1CA" id="svg_134"/>
<path d="m715.076,280.980881c-1.135,0 -1.906,0.215 -2.424,-0.055c-0.648,-0.338 -1.446,-1.033 -1.518,-1.651c-0.072,-0.612 0.568,-1.78 1.087,-1.904c1.89,-0.452 3.887,-1.119 5.789,-0.143c0.643,0.33 1.382,1.06 1.491,1.706c0.174,1.034 -0.71,1.569 -1.714,1.707c-1.056,0.147 -2.116,0.266 -2.711,0.34z" fill="#EDD1CA" id="svg_135"/>
<path d="m652.969,279.278881c-1.25,-0.064 -2.178,-0.031 -3.074,-0.181c-0.87,-0.146 -1.738,-0.696 -1.508,-1.636c0.165,-0.674 0.957,-1.671 1.5,-1.692c2.221,-0.083 4.462,0.08 6.676,0.329c0.48,0.054 1.229,0.845 1.219,1.282c-0.011,0.471 -0.695,1.191 -1.214,1.333c-1.265,0.348 -2.607,0.425 -3.599,0.565z" fill="#EDD1CA" id="svg_136"/>
<path d="m709.683,290.088881c0.787,0.211 1.882,0.354 2.833,0.801c1.011,0.476 1.098,1.653 0.147,2.201c-1.596,0.919 -3.36,1.443 -5.244,1.002c-0.962,-0.225 -1.728,-0.773 -1.698,-1.861c0.029,-1.04 0.724,-1.662 1.699,-1.888c0.64,-0.148 1.313,-0.154 2.263,-0.255z" fill="#EDD1CA" id="svg_137"/>
<path d="m735.998,252.586881c-0.79,-0.092 -1.214,-0.085 -1.604,-0.198c-1.005,-0.291 -1.885,-0.81 -1.904,-1.985c-0.02,-1.232 0.879,-1.808 1.937,-1.817c1.452,-0.012 2.972,0.005 4.34,0.418c1.423,0.429 1.466,1.845 0.137,2.548c-0.995,0.527 -2.14,0.771 -2.906,1.034z" fill="#EDD1CA" id="svg_138"/>
<path d="m736.343,271.796881c-2.273,-0.125 -3.097,-0.611 -3.183,-1.997c-0.078,-1.265 1.025,-2.049 3.079,-2.046c0.969,0.001 1.987,0.067 2.888,0.376c0.505,0.173 1.16,0.925 1.134,1.38c-0.034,0.591 -0.556,1.402 -1.089,1.659c-0.846,0.409 -1.876,0.438 -2.829,0.628z" fill="#EDD1CA" id="svg_139"/>
<path d="m685.329,281.473881c-0.519,-0.091 -1.172,-0.177 -1.811,-0.324c-0.899,-0.207 -1.443,-0.763 -1.482,-1.72c-0.039,-0.96 0.485,-1.524 1.335,-1.857c1.168,-0.457 4.739,0.394 5.578,1.336c0.727,0.816 0.512,1.843 -0.548,2.125c-0.947,0.252 -1.951,0.289 -3.072,0.44z" fill="#EDD1CA" id="svg_140"/>
<path d="m740.005,293.233881c-1.484,2.038 -4.82,2.573 -6.338,1.091c-0.458,-0.447 -0.802,-1.318 -0.712,-1.921c0.068,-0.455 0.896,-1.103 1.414,-1.13c2.041,-0.107 4.097,-0.01 5.636,1.96z" fill="#EDD1CA" id="svg_141"/>
<path d="m676.726,294.574881c-1.711,-0.04 -3.084,-1.214 -3.063,-2.22c0.021,-0.962 1.182,-1.772 2.604,-1.715c0.987,0.039 2.016,0.066 2.934,0.373c0.515,0.172 1.134,0.88 1.173,1.388c0.036,0.475 -0.525,1.241 -1.011,1.458c-0.96,0.43 -2.05,0.569 -2.637,0.716z" fill="#EDD1CA" id="svg_142"/>
<path d="m645.368,290.254881c0.633,0.063 1.151,0.028 1.608,0.179c0.814,0.269 1.658,0.774 1.413,1.727c-0.148,0.577 -0.82,1.246 -1.4,1.436c-0.992,0.324 -2.103,0.442 -3.152,0.39c-0.94,-0.046 -1.703,-0.571 -1.744,-1.718c-0.038,-1.047 0.646,-1.474 1.465,-1.719c0.622,-0.188 1.291,-0.216 1.81,-0.295z" fill="#EDD1CA" id="svg_143"/>
<path d="m274.546,264.670881c-0.023,2.518 -1.747,4.264 -4.225,4.279c-1.497,0.009 -2.755,-1.173 -2.755,-2.588c0,-2.289 2.2,-4.961 4.097,-4.975c1.785,-0.014 2.901,1.257 2.883,3.284z" fill="#5C869F" id="svg_145"/>
<path d="m300.5,258.759881c-0.01,1.443 -1.51,2.701 -3.254,2.729c-2.272,0.036 -3.682,-1.048 -3.628,-2.788c0.068,-2.185 1.488,-3.76 3.444,-3.82c1.704,-0.053 3.451,1.919 3.438,3.879z" fill="#5C869F" id="svg_146"/>
<path d="m288.982,264.797881c-0.042,2.309 -0.991,3.358 -2.981,3.296c-1.448,-0.045 -2.113,-0.562 -2.702,-2.102c-0.46,-1.2 -0.029,-2.816 1.055,-3.791c0.917,-0.825 2,-1.185 3.198,-0.77c1.111,0.384 1.468,1.319 1.43,3.367z" fill="#5E879F" id="svg_147"/>
<path d="m54.996,243.147881c-0.296,0.503 -0.591,1.005 -1.117,1.898c-0.385,-1.33 0.001,-1.917 0.546,-2.46c0.191,0.188 0.381,0.375 0.571,0.562z" fill="#B7D0DE" id="svg_148"/>
<path d="m54.996,243.147881c-0.19,-0.187 -0.381,-0.375 -0.571,-0.562c0.19,0.188 0.381,0.375 0.571,0.562z" fill="#FFF6F3" id="svg_149"/>
<path d="m85.732,206.903881c-0.237,0.025 -0.475,0.049 -0.712,0.074c0.092,-0.177 0.185,-0.353 0.276,-0.531c0.145,0.152 0.291,0.304 0.436,0.457z" fill="#FFF6F3" id="svg_150"/>
<path d="m38.938,269.085881c0.15,-0.185 0.299,-0.37 0.447,-0.557c0.061,0.06 0.122,0.123 0.183,0.187c-0.174,0.159 -0.349,0.318 -0.523,0.477l-0.107,-0.107z" fill="#B2CDDD" id="svg_151"/>
<path d="m39.045,269.192881c-0.085,0.101 -0.169,0.202 -0.254,0.303c0.049,-0.136 0.099,-0.272 0.148,-0.409c-0.001,-0.001 0.106,0.106 0.106,0.106z" fill="#B2CDDD" id="svg_152"/>
<path d="m152.512,257.161881c3.724,-3.191 7.427,-6.406 11.178,-9.566c5.138,-4.329 10.983,-7.111 17.688,-8.031c4.144,-0.569 8.325,-1.086 12.392,0.074c3.417,0.975 6.74,2.334 10.019,3.724c2.576,1.092 4.493,3.109 6.227,5.273c-0.363,0.164 -0.719,0.451 -1.09,0.473c-3.391,0.201 -6.791,0.268 -10.175,0.546c-4.61,0.379 -9.215,0.848 -13.808,1.394c-3.524,0.419 -7.03,0.992 -10.537,1.545c-3.095,0.488 -6.184,1.022 -9.267,1.587c-0.554,0.102 -1.122,0.37 -1.581,0.702c-1.272,0.919 -2.694,1.102 -4.212,1.261c-1.542,0.161 -3.047,0.703 -4.557,1.123c-0.786,0.218 -1.553,0.736 -2.277,-0.105z" fill="#FCF4F2" id="svg_153"/>
<path d="m511.621,227.118881c-3.305,-4.685 -4.944,-9.95 -5.001,-15.629c-0.044,-4.401 1.644,-8.317 4.647,-11.502c4.067,-4.313 8.555,-3.465 12.441,-0.361c3.542,2.83 5.056,6.77 5.474,11.091c0.534,5.52 -0.699,10.843 -2.418,16.059c-0.983,-0.041 -1.967,-0.142 -2.947,-0.111c-4.066,0.131 -8.131,0.299 -12.196,0.453z" fill="#FDFDFE" id="svg_154"/>
<path d="m651.961,221.104881c0.457,1.649 1.046,3.198 1.302,4.8c1.302,8.173 -0.427,15.373 -7.132,20.786c-1.133,0.914 -2.531,1.579 -3.906,2.097c-2.092,0.788 -3.43,0.253 -4.71,-1.598c-1.317,-1.906 -1.929,-4.125 -1.956,-6.347c-0.043,-3.592 0.49,-7.119 2.83,-10.114c3.502,-4.483 7.622,-8.064 13.572,-9.624z" fill="#FCDACF" id="svg_155"/>
<path d="m511.621,227.118881c4.065,-0.154 8.13,-0.323 12.197,-0.451c0.98,-0.031 1.964,0.07 2.947,0.111c-1.078,3.612 -2.945,6.8 -5.242,10.06c-4.142,-2.56 -7.428,-5.713 -9.902,-9.72z" fill="#FCDACF" id="svg_156"/>
<path d="m137.585,256.566881c-0.232,1.81 -0.466,3.621 -0.694,5.431c-0.152,1.21 -0.755,1.98 -2.018,2.163c-2.277,0.33 -4.455,1.03 -6.873,0.68c-4.262,-0.616 -6.739,-3.083 -7.888,-6.96c-1.494,-5.037 -0.23,-9.754 2.481,-14.042c2.013,-3.184 5.467,-3.625 8.715,-1.513c3.204,2.084 4.885,5.144 5.587,8.784c0.345,1.788 0.465,3.616 0.69,5.457z" fill="#FCF4F2" id="svg_163"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+179
View File
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="180" height="140" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<g id="svg_232">
<g id="svg_233">
<g id="svg_234">
<path d="m76.933,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0,0 2.8,2.119 2.8,1.413z" fill="#3F3945" id="svg_235"/>
<path d="m81.83,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0.001,0 2.8,2.119 2.8,1.413z" fill="#3F3945" id="svg_236"/>
</g>
<g id="svg_237">
<path d="m98.866,96.939l6.102,30.954l0.712,0l-4.014,-32.367c-0.001,0 -2.8,2.119 -2.8,1.413z" fill="#3F3945" id="svg_238"/>
<path d="m93.968,96.939l6.102,30.954l0.712,0l-4.014,-32.367c0,0 -2.8,2.119 -2.8,1.413z" fill="#3F3945" id="svg_239"/>
</g>
</g>
<path d="m103.771,77.118l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.748,0c-1.852,0 -3.353,1.501 -3.353,3.353l0,5.309c0,6.848 5.551,12.399 12.399,12.399l7.917,0l1.864,0l5.365,0c6.848,0 12.399,-5.551 12.399,-12.399l0,-5.309c0,-1.852 -1.501,-3.353 -3.353,-3.353l-0.746,0z" fill="#9292AA" id="svg_240"/>
<g id="svg_241">
<g id="svg_242">
<rect fill="#3F3945" height="30.178" id="svg_243" transform="matrix(0.374 -0.9274 0.9274 0.374 31.4581 419.478)" width="0.486" x="310.463764" y="-78.932512"/>
</g>
<g id="svg_244">
<rect fill="#3F3945" height="0.486" id="svg_245" transform="matrix(0.9271 -0.3748 0.3748 0.9271 -46.1495 135.573)" width="30.178" x="119.351398" y="23.748997"/>
</g>
</g>
<path d="m103.942,77.118l-0.171,0l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.01,0l0,12.579c0,4.58 3.633,8.447 8.213,8.482c0.032,0 0.064,0 0.096,0l7.917,0l1.864,0l5.365,0c0.088,0 0.176,-0.001 0.264,-0.003c4.577,-0.096 8.206,-3.907 8.206,-8.485l0,-12.573z" fill="#CFCCDF" id="svg_246"/>
<path d="m70.573,93.419c2.27,2.894 5.79,4.76 9.753,4.76l7.917,0l1.864,0l5.365,0c3.963,0 7.483,-1.866 9.753,-4.76l-34.652,0z" fill="#9292AA" id="svg_247"/>
</g>
<g id="svg_345">
<polygon fill="#D7914E" id="svg_346" points="105.989990234375,118.74800109863281 106.02999877929688,125.25300598144531 106.30999755859375,127.28900146484375 109.94100952148438,127.48800659179688 110.94900512695312,126.00199890136719 110.30099487304688,117.8699951171875 "/>
<rect fill="#3F3945" height="15.55" id="svg_347" width="21.717" x="76.059" y="78.354"/>
<path d="m68.326,77.424c-0.902,-0.263 -1.782,-0.6 -2.629,-1.005c-1.383,-0.661 -2.848,-1.829 -2.702,-3.355c0.096,-1.006 0.995,-3.146 1.705,-3.865c1.915,-1.94 7.913,-13.124 9.945,-14.721c0.888,-0.698 9.498,-4.708 9.904,-2.435c0.077,0.43 -0.507,3.353 -0.629,3.772c-0.972,3.357 -3.12,4.862 -5.219,7.656c-1.819,2.422 -4.013,4.552 -5.728,7.048c0.632,1.036 1.411,2.12 2.385,2.843c3.033,2.25 6.071,4.491 9.116,6.724c-0.729,0.863 -1.156,1.903 -1.474,2.987c-5.004,-1.626 -9.621,-4.173 -14.674,-5.649z" fill="#D7914E" id="svg_348"/>
<path d="m103.921,77.424c0.902,-0.263 1.782,-0.6 2.629,-1.005c1.383,-0.661 2.848,-1.829 2.702,-3.355c-0.096,-1.006 -0.995,-3.146 -1.705,-3.865c-1.915,-1.94 -7.913,-13.124 -9.945,-14.721c-0.888,-0.698 -9.498,-4.708 -9.904,-2.435c-0.077,0.43 0.507,3.353 0.629,3.772c0.972,3.357 3.12,4.862 5.219,7.656c1.819,2.422 4.013,4.552 5.728,7.048c-0.632,1.036 -1.411,2.12 -2.385,2.843c-3.033,2.25 -6.071,4.491 -9.116,6.724c0.729,0.863 1.156,1.903 1.474,2.987c5.004,-1.626 9.621,-4.173 14.674,-5.649z" fill="#D7914E" id="svg_349"/>
<path d="m92.764,74.378c0,1.592 -1.348,2.882 -3.011,2.882c-1.663,0 -3.011,-1.29 -3.011,-2.882c0,-1.592 1.348,-2.882 3.011,-2.882c1.663,-0.001 3.011,1.29 3.011,2.882z" fill="#A0616B" id="svg_350"/>
<g id="svg_351">
<path d="m104.514,121.741l-0.079,-1.984c-0.668,-10.878 -2.243,-21.139 -3.588,-24.237c-1.649,-1.06 -7.638,-2.66 -13.028,-3.43l-4.808,-0.687l1.373,-9.615l4.808,0.687c4.352,0.622 14.889,2.484 18.744,6.338c1.096,1.095 4.427,1.511 6.193,30.349l-0.116,2.139l-9.499,0.44zm-3.804,-26.502l0,0l0,0z" fill="#3F3945" id="svg_352"/>
</g>
<g id="svg_353">
<path d="m75.533,124.821l-0.868,-4.778c-0.162,-0.892 -2.938,-16.203 -3.432,-23.825c-0.527,-8.127 4.433,-11.396 7.13,-11.99l4.743,-1.044l2.088,9.485l-4.083,0.899c-0.103,0.258 -0.261,0.86 -0.186,2.021c0.458,7.067 3.258,22.503 3.296,22.716l0.869,4.778l-9.557,1.738z" fill="#3F3945" id="svg_354"/>
</g>
<path d="m99.534,58.12c-0.233,-3.776 -5.585,-7.346 -12.204,-7.346c-6.619,0 -12.365,1.538 -13.534,5.16c-0.585,1.811 2.424,21.23 2.401,22.425c-0.075,3.845 -0.257,5.671 -0.257,5.671l21.993,0.187c0,0 0.083,-2.617 -0.09,-5.498c-0.11,-1.83 1.812,-18.636 1.691,-20.599z" fill="#D7914E" id="svg_355"/>
<path d="m86.871,53.229l-0.575,0c-1.537,0 -2.783,-1.246 -2.783,-2.783l0,-8.716l6.141,0l0,8.716c0,1.537 -1.246,2.783 -2.783,2.783z" fill="#E8A8B8" id="svg_356"/>
<path d="m83.514,44.073c0.813,2.108 2.586,5.356 6.14,6.155l0,-8.499l-5.55,0l-0.59,0.259l0,2.085z" fill="#D98E9D" id="svg_357" opacity="0.5"/>
<path d="m88.507,31.913l-1.4,0.188c-2.784,0.374 -4.738,2.933 -4.364,5.717l0.64,4.77c0.378,2.815 2.966,4.791 5.781,4.413l2.485,-0.334c2.154,-0.289 3.665,-2.269 3.376,-4.423l-0.801,-5.967c-0.373,-2.783 -2.933,-4.737 -5.717,-4.364z" fill="#E8A8B8" id="svg_358"/>
<path d="m85.319,41.204c0.201,1.498 -0.85,2.874 -2.348,3.075c-1.497,0.201 -2.874,-0.85 -3.075,-2.348c-0.201,-1.498 0.85,-2.874 2.348,-3.075c1.497,-0.201 2.874,0.85 3.075,2.348z" fill="#E8A8B8" id="svg_359"/>
<path d="m82.98,39.098c0.342,0.948 0.611,1.922 0.805,2.91c0.098,-0.121 0.301,-0.197 0.452,-0.161" fill="none" id="svg_360"/>
<path d="m79.405,36.266c0.064,0.203 0.171,0.389 0.277,0.573c0.448,0.779 0.895,1.558 1.343,2.337c0.533,-0.343 1.228,-0.251 1.862,-0.25c0.168,0.696 0.253,1.418 0.279,2.134c0.23,-0.047 0.468,-0.064 0.703,-0.052c-0.081,-0.824 -0.109,-1.66 -0.135,-2.487c0.207,-0.25 0.524,-0.439 0.753,-0.669c-0.368,-0.4 -0.571,-0.938 -0.615,-1.479c-0.056,-0.687 0.26,-1.236 0.6,-1.836c0.34,-0.6 0.843,-1.132 1.487,-1.379c1.383,-0.531 2.731,-1.006 4.207,-0.891c0.582,0.045 1.221,0.088 1.716,0.399c0.765,-0.017 1.343,-0.848 1.326,-1.613c-0.017,-0.765 -0.473,-1.453 -1.003,-2.004c-0.374,-0.389 -0.797,-0.736 -1.275,-0.987c-0.595,-0.313 -1.261,-0.47 -1.928,-0.554c-1.757,-0.221 -3.575,0.058 -5.185,0.796c0.294,0.06 0.505,0.162 0.799,0.222c-0.498,0.05 -0.985,0.172 -1.483,0.222c-0.53,0.053 -1.065,0.108 -1.57,0.277c-0.922,0.309 -1.687,0.985 -2.259,1.772c-0.572,0.787 -0.969,1.684 -1.345,2.581c-0.163,0.389 -0.326,0.803 -0.264,1.22c0.051,0.347 0.254,0.652 0.469,0.93c0.272,0.349 0.797,0.732 1.241,0.738z" fill="#3F3945" id="svg_361"/>
<path d="m90.769,42.967l-2.971,-0.714c0,0 0.493,1.603 1.441,1.755c0.947,0.15 1.53,-1.041 1.53,-1.041z" fill="#FFFFFF" id="svg_362"/>
<path d="m89.656,38.335c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.04 -0.58,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.302,-0.041 0.579,0.171 0.62,0.473z" fill="#3F3945" id="svg_363"/>
<path d="m93.483,37.821c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.041 -0.579,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.301,-0.041 0.579,0.171 0.62,0.473z" fill="#3F3945" id="svg_364"/>
<polygon fill="#FFFFFF" id="svg_365" points="86.84201049804688,52.28199768066406 82.7130126953125,51.47599792480469 83.10000610351562,54.055999755859375 87.48599243164062,60.11799621582031 91.87100219726562,54.82899475097656 89.93600463867188,51.08900451660156 "/>
<path d="m83.382,49.978c0,0 3.161,1.747 6.287,0l0.359,1.527l-0.773,3.712l-2.315,-2.559l-1.996,2.689l-2.102,-3.483l0.54,-1.886z" fill="#EFF2F7" id="svg_366"/>
<g id="svg_367">
<path d="m77.129,132.034c0,0 -0.359,1.321 -0.166,2.048c0.193,0.728 0.945,2.038 2.77,2.094c1.825,0.057 4.203,0.173 4.061,-4.926l-6.665,0.784z" fill="#525560" id="svg_368"/>
<path d="m79.816,123.896c0.162,-0.288 -0.354,-2.445 1.152,-2.371c1.506,0.074 1.678,0.793 1.567,1.853c-0.111,1.06 -0.807,5.388 -0.807,5.388l-2.112,0.193l0.2,-5.063z" fill="#3F3945" id="svg_369"/>
<path d="m79.5,122.394c0,0 -0.642,3.727 -0.709,3.871c-0.067,0.145 -1.992,5.885 -1.714,6.789c0.2,0.651 1.325,1.583 3.732,1.492c1.852,-0.07 3.219,-1.763 2.945,-3.596c-0.243,-1.626 -0.549,-4.7 -0.547,-4.746c0.005,-0.089 -0.298,-3.866 -0.298,-3.866s-1.764,2.646 -1.904,5.904l-0.65,-0.035c-0.001,0.001 0.162,-5.996 -0.855,-5.813z" fill="#3F3945" id="svg_370"/>
<g id="svg_371">
<path d="m82.319,124.27c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.056 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.748,-0.955 2.831,0.148c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.045 -0.107,0.068 -0.168,0.068z" fill="#525560" id="svg_372"/>
</g>
<g id="svg_373">
<path d="m82.161,125.502c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#525560" id="svg_374"/>
</g>
<g id="svg_375">
<path d="m82.003,126.735c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.037,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.102 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.091,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#525560" id="svg_376"/>
</g>
<g id="svg_377">
<path d="m81.845,127.968c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.338,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.048,0.046 -0.109,0.069 -0.169,0.069z" fill="#525560" id="svg_378"/>
</g>
</g>
<g id="svg_379">
<path d="m109.648,124.917c0,0 -0.906,-1.496 -0.021,-1.749c0.885,-0.253 1.434,0.241 1.434,0.241l2.491,2.198l1.365,1.652l-1.831,-0.581l-3.438,-1.761z" fill="#3F3945" id="svg_380"/>
<path d="m106.055,124.579c0,0 0.944,1.669 1.662,1.261c0.406,-0.231 0.985,-1.755 1.573,-1.617c0.588,0.138 2.961,1.476 3.837,1.794c0.876,0.318 -2.173,-2.574 -2.173,-2.574s2.521,1.913 3.304,2.2c0.784,0.286 7.197,1.07 7.591,2.986l0.064,1.261c0,0 -1.789,2.128 -6.783,1.15c-4.995,-0.978 -7.744,-0.907 -8.65,-1.133c-0.907,-0.227 -1.107,-5.191 -0.425,-5.328z" fill="#3F3945" id="svg_381"/>
<path d="m116.03,130.259c-2.331,-0.074 -6.343,-1.231 -7.828,-1.202c-0.855,0.017 -1.737,-0.166 -2.331,-0.325c0.14,0.637 0.344,1.108 0.609,1.174c0.907,0.226 3.656,0.155 8.65,1.133c4.995,0.978 6.783,-1.15 6.783,-1.15l-0.064,-1.261c-0.423,1.066 -3.488,1.705 -5.819,1.631z" fill="#525560" id="svg_382"/>
<g id="svg_383">
<path d="m111.068,126.016c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.032,0.086 -0.108,0.142 -0.192,0.153z" fill="#525560" id="svg_384"/>
</g>
<g id="svg_385">
<path d="m111.893,126.603c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.296,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.878,1.073c-0.032,0.086 -0.107,0.142 -0.191,0.153z" fill="#525560" id="svg_386"/>
</g>
<g id="svg_387">
<path d="m112.861,126.952c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.1 0.612,-0.058 0.908,0.124c0.113,0.069 0.149,0.217 0.079,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.108 -0.339,-0.133 -0.502,-0.078c-0.435,0.147 -0.781,0.824 -0.878,1.073c-0.032,0.085 -0.107,0.141 -0.191,0.152z" fill="#525560" id="svg_388"/>
</g>
<g id="svg_389">
<path d="m113.865,127.252c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.103 0.611,-0.058 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.107 -0.339,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.033,0.086 -0.108,0.141 -0.192,0.153z" fill="#525560" id="svg_390"/>
</g>
</g>
<path d="m85.292,83.099c-0.866,-0.069 -1.783,-0.265 -2.389,-0.888c-0.606,-0.622 0.539,-1.594 1.239,-2.108c0.968,0.44 2.076,-0.063 3.133,-0.182c0.641,-0.072 1.289,0.003 1.926,0.105c0.817,0.131 1.636,0.311 2.374,0.685c0.738,0.374 1.394,0.96 1.713,1.723c0.075,0.179 -0.024,0.35 -0.217,0.353c-0.194,0.003 -0.358,-0.133 -0.51,-0.253c-0.477,-0.376 -1.026,-0.66 -1.608,-0.834c-0.105,-0.031 -0.185,0.144 -0.086,0.191c1.456,0.689 1.801,1.611 1.775,1.694c-0.028,0.09 -0.139,0.124 -0.234,0.122c-0.171,-0.003 -0.452,-0.259 -0.594,-0.354c-0.603,-0.399 -1.146,-0.472 -1.146,-0.472s-0.619,0.036 0.899,0.801c0.187,0.094 0.381,0.221 0.443,0.421c0.011,0.035 0.017,0.073 0.01,0.109c-0.008,0.043 -0.035,0.081 -0.067,0.112c-0.098,0.093 -0.242,0.124 -0.376,0.112c-0.134,-0.011 -0.262,-0.06 -0.388,-0.109c-0.618,-0.237 -1.235,-0.475 -1.853,-0.712c0.163,0.103 1.282,0.662 1.403,0.813c0.047,0.059 0.092,0.131 0.077,0.204c-0.023,0.109 -0.157,0.146 -0.268,0.157c-0.743,0.074 -2.45,-0.542 -3.146,-0.813c-0.695,-0.269 -1.365,-0.818 -2.11,-0.877z" fill="#E8A8B8" id="svg_391"/>
<g id="svg_392">
<path d="m87.677,40.908c-1.249,0 -2.339,-0.926 -2.51,-2.197c-0.09,-0.671 0.086,-1.337 0.497,-1.875c0.411,-0.538 1.006,-0.884 1.677,-0.974c1.386,-0.186 2.663,0.79 2.849,2.174c0.186,1.385 -0.79,2.662 -2.175,2.849c-0.113,0.016 -0.226,0.023 -0.338,0.023zm0.003,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-0.606,0.081 -1.145,0.394 -1.516,0.88c-0.372,0.487 -0.531,1.089 -0.45,1.695c0.169,1.252 1.324,2.137 2.575,1.966c1.252,-0.168 2.134,-1.324 1.966,-2.576c-0.154,-1.148 -1.139,-1.986 -2.269,-1.986z" fill="#3F3945" id="svg_393"/>
</g>
<g id="svg_394">
<path d="m93.067,40.184c-0.552,0 -1.086,-0.179 -1.532,-0.52c-0.538,-0.411 -0.884,-1.006 -0.974,-1.677c-0.186,-1.385 0.789,-2.663 2.174,-2.849c1.384,-0.184 2.663,0.79 2.849,2.175c0.09,0.671 -0.086,1.336 -0.497,1.874c-0.411,0.538 -1.006,0.884 -1.678,0.974c-0.114,0.015 -0.228,0.023 -0.342,0.023zm0.007,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-1.252,0.168 -2.133,1.324 -1.965,2.576c0.082,0.607 0.394,1.145 0.88,1.517c0.488,0.372 1.094,0.532 1.695,0.449c0.607,-0.081 1.146,-0.394 1.517,-0.88c0.372,-0.487 0.531,-1.088 0.45,-1.695c-0.156,-1.15 -1.141,-1.988 -2.271,-1.988z" fill="#3F3945" id="svg_395"/>
</g>
<g id="svg_396">
<rect fill="#3F3945" height="0.242" id="svg_397" transform="matrix(0.9187 -0.3949 0.3949 0.9187 -20.6624 136.864)" width="2.505" x="133.483205" y="-48.057873"/>
</g>
<g id="svg_398">
<path d="m90.262,87.938l20.542,0l0,-1.506l-20.542,0c-0.416,0 -0.753,0.337 -0.753,0.753l0,0c0,0.415 0.337,0.753 0.753,0.753z" fill="#A8A5C4" id="svg_399"/>
<path d="m92.011,87.938l19.809,0c0.876,0 1.643,-0.59 1.867,-1.436l3.496,-13.172c0.325,-1.226 -0.599,-2.428 -1.867,-2.428l-17.297,0c-0.876,0 -1.643,0.589 -1.867,1.436l-4.141,15.6z" fill="#CFCCDF" id="svg_400"/>
<path d="m115.315,70.902c1.268,0 2.193,1.202 1.867,2.428l-3.496,13.172c-0.108,0.406 -0.342,0.751 -0.652,1.002l-12.041,-16.601l14.322,0l0,-0.001z" fill="#CFCCDF" id="svg_401"/>
<path d="m103.676,80.586c-0.217,0.859 0.324,1.556 1.208,1.556c0.884,0 1.776,-0.696 1.993,-1.556c0.217,-0.859 -0.324,-1.556 -1.208,-1.556c-0.884,0 -1.776,0.697 -1.993,1.556z" fill="#FFFFFF" id="svg_402"/>
</g>
</g>
<g id="svg_403">
<path d="m126.823,42.216l-2.049,-3.268c0,0 0.64,-1.378 -0.06,-2.56c-0.699,-1.182 -1.769,-3.318 -2.053,-3.046s0.287,2.342 0.287,2.342s-1.883,-3.209 -3.159,-3.401c-1.276,-0.193 1.325,3.309 1.325,3.309s-2.943,-3.063 -3.122,-2.455s1.769,3.318 1.933,3.488c0.164,0.17 -2.237,-2.215 -2.411,-1.829c-0.175,0.386 2.694,4.394 3.295,4.906c0.602,0.513 5.102,3.607 5.102,3.607l0.912,-1.093z" fill="#E8A8B8" id="svg_404"/>
<path d="m152.166,57.969l-0.035,-0.084l-9.44,-2.281l-1.141,4.72c-0.16,0.665 -4.486,17.513 -4.967,25.197c-0.286,4.586 11.326,34.167 12.018,35.98l0.099,1.43l-1.833,2.344c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.9,-1.025 2.9,-1.025l-1.123,-4.079c0,0 -3.448,-21.082 -4.531,-27.574c-0.438,-2.627 -1.976,-4.794 -1.954,-5.541c0.256,-4.092 3.397,-11.984 5.887,-17.698c-0.193,5.235 -0.476,12.31 -0.735,16.441c-0.282,4.514 4.782,36.055 4.784,37.293l0.006,-0.012l-0.008,0.041l-2.354,2.58c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.573,-1.911 2.573,-1.911s1.925,-36.981 2.142,-40.473c0.463,-7.413 0.469,-20.196 0.469,-20.737l0,-4.856l-8.889,0l0,0.001z" fill="#E8A8B8" id="svg_405"/>
<path d="m141.222,34.996c-0.081,1.39 0.046,2.794 -0.178,4.168c-0.224,1.374 -0.881,2.774 -2.104,3.438c-1.778,0.965 -3.985,0.055 -5.662,-1.077c-1.677,-1.132 -3.352,-2.546 -5.374,-2.628c-2.174,-0.088 -4.267,1.672 -4.554,3.829c-0.278,2.09 1.001,4.141 2.706,5.38c1.706,1.239 3.786,1.824 5.818,2.387c2.631,0.728 5.511,1.443 8.015,0.354c1.623,-0.706 2.845,-2.08 3.982,-3.437c2.616,-3.121 5.148,-6.53 5.932,-10.527c0.536,-2.731 0.007,-8.52 -3.222,-9.671c-3.815,-1.359 -5.207,5.17 -5.359,7.784z" fill="#5387C4" id="svg_406"/>
<path d="m163.17,56.037l0.452,10.385l-2.032,17.61l-25.06,-1.355l0.249,-2.57c0.734,-7.587 2.437,-15.048 5.067,-22.202l0.846,-2.301l20.478,0.433z" fill="#DC614B" id="svg_407"/>
<path d="m166.608,27.766c-1.695,-1.277 -2.558,-3.404 -2.871,-5.503c-0.313,-2.1 -0.152,-4.237 -0.217,-6.359c-0.046,-1.511 -0.218,-3.057 -0.898,-4.407c-0.68,-1.35 -1.955,-2.479 -3.46,-2.629c-0.924,-0.092 -1.869,0.183 -2.779,-0.006c-0.198,0.405 -0.265,0.944 0.064,1.251c0.33,0.307 0.747,0.518 1.037,0.863c0.396,0.469 0.501,1.116 0.519,1.729c0.082,2.845 -1.37,5.539 -1.477,8.384c-0.052,1.381 0.227,2.787 0.901,3.993c0.674,1.206 1.758,2.199 3.058,2.666c1.841,0.659 4.217,0.449 6.123,0.018z" fill="#3F3945" id="svg_408"/>
<path d="m148.76,25.747l1.141,-0.145c0,0 1.821,1.121 2.287,1.037c1.09,-0.195 1.289,-0.589 1.289,-0.589s1.077,-0.719 1.563,-0.685c6.145,0.436 5.828,4.114 6.358,6.234c0.928,3.716 -2.395,9.115 -2.395,9.115s4.126,13.236 3.942,15.548l-11.556,1.9c-3.17,0.521 -6.421,-0.151 -9.124,-1.887l0,0l1.264,-11.978l-0.833,-2.272c-1.126,-3.07 -1.414,-6.387 -0.821,-9.603c0.253,-1.368 0.523,-2.637 0.751,-3.252c0.644,-1.736 4.295,-3.189 6.134,-3.423z" fill="#5387C4" id="svg_409"/>
<g id="svg_410">
<g id="svg_411">
<path d="m152.026,28.777l0,0c1.696,0 3.07,-1.375 3.07,-3.07l0,-8.061c0,-0.203 -0.165,-0.367 -0.367,-0.367l-5.406,0c-0.203,0 -0.367,0.164 -0.367,0.367l0,8.061c-0.001,1.695 1.374,3.07 3.07,3.07z" fill="#E8A8B8" id="svg_412"/>
<path d="m155.096,19.621c-0.813,2.108 -2.586,5.356 -6.141,6.155l0,-8.499l5.55,0l0.59,0.259l0,2.085l0.001,0z" fill="#D98E9D" id="svg_413" opacity="0.5"/>
</g>
<path d="m147.55,8.2l1.413,0c2.809,0 5.086,2.277 5.086,5.086l0,4.812c0,2.84 -2.302,5.143 -5.143,5.143l-2.507,0c-2.173,0 -3.934,-1.761 -3.934,-3.934l0,-6.021c0,-2.809 2.277,-5.086 5.085,-5.086z" fill="#E8A8B8" id="svg_414"/>
<circle cx="154.682" cy="16.984" fill="#E8A8B8" id="svg_415" r="2.736"/>
<path d="m153.984,14.586c-0.213,0.985 -0.35,1.986 -0.41,2.991c-0.114,-0.107 -0.325,-0.155 -0.47,-0.099" fill="none" id="svg_416"/>
<path d="m145.848,19.027l2.962,-0.753c0,0 -0.472,1.61 -1.418,1.773c-0.947,0.163 -1.544,-1.02 -1.544,-1.02z" fill="#FFFFFF" id="svg_417"/>
<path d="m146.767,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#3F3945" id="svg_418"/>
<path d="m142.905,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#3F3945" id="svg_419"/>
<path d="m151.485,8.973c0,0 -1.112,2.88 0.312,4.687c0.434,0.551 0.784,2.299 0.931,2.984c0.153,0 0.321,-1.654 1.2,-2.207c0.304,-0.195 1.535,-0.114 1.535,-0.114s0.673,-1.691 0.818,-2.126c0.185,-0.554 0.492,-1.063 0.666,-1.62c0.581,-1.859 -0.652,-4.078 -2.537,-4.567c-0.551,-0.143 -1.122,-0.216 -1.691,-0.236c-12.074,-0.429 -10.65,5.234 -10.605,6.527c5.007,0.668 9.371,-3.328 9.371,-3.328z" fill="#3F3945" id="svg_420"/>
</g>
<path d="m163.38,57.685c0.802,-0.333 1.613,-0.804 1.996,-1.584c0.383,-0.78 -1.006,-1.349 -1.831,-1.621c-0.785,0.718 -1.993,0.582 -3.035,0.797c-0.632,0.13 -1.225,0.402 -1.799,0.696c-0.736,0.377 -1.46,0.802 -2.046,1.386c-0.586,0.584 -1.028,1.344 -1.095,2.169c-0.016,0.193 0.131,0.325 0.316,0.269c0.185,-0.057 0.299,-0.237 0.407,-0.398c0.337,-0.505 0.771,-0.945 1.27,-1.29c0.091,-0.063 0.221,0.08 0.141,0.155c-1.171,1.105 -1.214,2.089 -1.164,2.16c0.055,0.077 0.171,0.074 0.26,0.044c0.161,-0.056 0.349,-0.387 0.455,-0.52c0.45,-0.566 0.944,-0.804 0.944,-0.804s0.6,-0.157 -0.607,1.04c-0.148,0.147 -0.293,0.329 -0.291,0.538c0,0.037 0.006,0.074 0.024,0.106c0.021,0.038 0.059,0.066 0.098,0.085c0.122,0.058 0.268,0.043 0.392,-0.009c0.124,-0.052 0.231,-0.139 0.335,-0.223c0.514,-0.417 1.027,-0.834 1.541,-1.251c-0.123,0.149 -1.014,1.026 -1.082,1.207c-0.026,0.07 -0.047,0.153 -0.009,0.218c0.056,0.096 0.195,0.09 0.303,0.066c0.73,-0.16 2.161,-1.274 2.74,-1.746c0.58,-0.473 1.047,-1.203 1.737,-1.49z" fill="#E8A8B8" id="svg_421"/>
<path d="m163.052,29.686c2.636,2.75 5.273,5.5 7.909,8.25c2.443,2.548 5.045,5.473 5.038,9.003c-0.005,2.685 -1.557,5.122 -3.318,7.148c-1.249,1.437 -2.708,2.804 -4.535,3.338c-1.827,0.535 -4.086,-0.005 -5.022,-1.663c-1.015,-1.796 -0.176,-4.044 0.782,-5.871c0.958,-1.827 2.065,-3.848 1.519,-5.837c-0.388,-1.416 -1.538,-2.469 -2.585,-3.498c-2.957,-2.908 -5.618,-6.529 -6.459,-10.668c-0.357,-1.759 -0.916,-3.975 1.325,-3.962c1.979,0.012 4.113,2.473 5.346,3.76z" fill="#5387C4" id="svg_422"/>
<path d="m148.515,121.53l-1.375,3.425l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l11.019,-0.762l-1.007,-6.275l-3.829,0.317z" fill="#3F3945" id="svg_423"/>
<path d="m152.805,125.823l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l10.017,-0.755l0.776,-6.717l-3.943,-0.403l0.317,1.9l-2.359,2.68z" fill="#3F3945" id="svg_424"/>
</g>
<g id="svg_425">
<path d="m24.854,55.079c4.549,0.076 9.099,0.153 13.648,0.229c1.546,0.026 3.103,0.051 4.624,-0.227c3.221,-0.588 11.197,-8.695 13.435,-10.265c0.838,0.743 1.646,1.432 2.484,2.174c-2.347,4.501 -3.586,7.027 -6.704,11.032c-0.932,1.197 -1.938,2.402 -3.293,3.082c-1.054,0.528 -2.246,0.7 -3.417,0.839c-4.995,0.59 -10.047,0.696 -15.063,0.317c-0.661,-0.05 -1.359,-0.122 -1.888,-0.522c-0.461,-0.348 -0.723,-0.894 -0.967,-1.418c-1.162,-2.485 -1.699,-2.754 -2.859,-5.241z" fill="#EDA956" id="svg_426"/>
<path d="m27.581,35.701c0,0 -7.203,0.647 -9.146,7.365c-1.943,6.718 1.781,9.308 1.781,9.308l1.376,0.081l0.648,-1.295l0.486,1.295l8.579,0.971l0.567,-0.648l0.405,0.648c0,0 4.209,-1.295 3.966,-4.694c-0.243,-3.399 -0.081,-5.018 -1.133,-7.041c-1.052,-2.023 -2.914,-4.452 -2.914,-4.452l-4.615,-1.538z" fill="#563238" id="svg_427"/>
<g id="svg_428">
<path d="m35.733,93.744l4.374,34.965l0.712,0l-2.287,-36.243c0.001,0.001 -2.799,1.917 -2.799,1.278z" fill="#3F3945" id="svg_429"/>
<path d="m45.446,92.449l16.677,36.26l0.712,0l-14.589,-37.538c0,0.001 -2.8,1.917 -2.8,1.278z" fill="#3F3945" id="svg_430"/>
<path d="m23.661,92.449l-16.677,36.26l-0.712,0l14.589,-37.538c0.001,0.001 2.8,1.917 2.8,1.278z" fill="#3F3945" id="svg_431"/>
<path d="m31.755,93.744l-4.374,34.965l-0.712,0l2.287,-36.243c-0.001,0.001 2.799,1.917 2.799,1.278z" fill="#3F3945" id="svg_432"/>
<g id="svg_433">
<rect fill="#3F3945" height="0.485" id="svg_434" transform="matrix(0.4675 -0.884 0.884 0.4675 -9.1243 344.263)" width="19.406" x="230.048508" y="-67.858469"/>
</g>
<g id="svg_435">
<rect fill="#3F3945" height="22.897" id="svg_436" transform="matrix(0.6928 -0.7211 0.7211 0.6928 -43.4672 259.313)" width="0.485" x="174.469854" y="-57.134714"/>
</g>
<g id="svg_437">
<rect fill="#3F3945" height="0.486" id="svg_438" transform="matrix(0.6874 -0.7263 0.7263 0.6874 -50.5056 245.956)" width="24.505" x="141.987717" y="-45.752078"/>
</g>
<g id="svg_439">
<polygon fill="#3F3945" id="svg_440" points="30.022003173828125,107.79200744628906 20.957000732421875,92.89999389648438 21.37298583984375,92.64700317382812 30.43798828125,107.53900146484375 "/>
</g>
<path d="m18.899,55.868c0,0 10.684,-2.59 14.245,19.101l0.56,2.427c0.816,3.538 3.897,6.093 7.525,6.241l4.676,0.191c3.044,0.124 5.701,2.099 6.697,4.978l1.262,3.645l2.586,0c0,0 -0.971,5.18 -6.151,5.18c-5.18,0 -17.155,0 -17.155,0l-11.007,0c0,0 -9.065,0.648 -10.684,-11.331c-1.619,-11.979 -7.447,-31.08 7.446,-30.432z" fill="#5A8ECC" id="svg_441"/>
<path d="m59.2,124.768l2.354,2.58l3.653,1.66c1.819,0.738 1.155,1.635 1.155,1.635l-10.017,-0.755l-0.757,-3.442l3.612,-1.678z" fill="#3F3945" id="svg_442"/>
<path d="m55.342,116.368l3.858,8.4l2.354,2.58c0,0 -1.511,0.448 -3.066,0.122c-1.555,-0.326 -2.9,-1.025 -2.9,-1.025l-4.08,-6.569l-0.47,-3.327l4.304,-0.181z" fill="#E8A8B8" id="svg_443"/>
<polygon fill="#E8A8B8" id="svg_444" points="46.364990234375,119.7550048828125 43.110992431640625,122.447998046875 43.89599609375,126.93699645996094 46.701995849609375,128.0590057373047 41.989013671875,127.94700622558594 39.519989013671875,121.55099487304688 42.217010498046875,118.19599914550781 "/>
<path d="m46.702,128.059c0,0 0.991,0.013 1.331,0.391c0.34,0.377 0,0.944 0,0.944l-7.615,-0.213l-2.581,-7.406l1.683,-0.224l2.469,6.396l4.713,0.112z" fill="#3F3945" id="svg_445"/>
<g id="svg_446">
<path d="m47.267,120.77l-7.793,-3.96l1.98,-3.896c3.337,-6.565 6.933,-14.445 8.429,-18.673c-4.337,-0.627 -12.462,-1.279 -19.433,-1.635l-4.365,-0.222l0.446,-8.73l4.365,0.222c23.576,1.202 25.569,2.862 26.639,3.754c3.192,2.661 2.048,6.952 -2.774,17.706c-2.604,5.804 -5.396,11.306 -5.513,11.538l-1.981,3.896zm5.396,-25.969l0,0l0,0z" fill="#9D3542" id="svg_447"/>
</g>
<path d="m38.418,93.753l-8.349,0.463c-4.148,0 -9.259,-3.453 -9.259,-7.601l-0.124,-2.204l16.226,-1.472l1.506,10.814z" fill="#9D3542" id="svg_448"/>
<g id="svg_449">
<path d="m49.849,123.164l-0.339,-4.357c-0.68,-8.739 -2.282,-19.42 -3.618,-21.755c-1.457,-1.115 -7.727,-2.828 -13.42,-3.641l-4.326,-0.618l1.236,-8.653l4.327,0.618c4.303,0.615 14.717,2.449 18.469,6.2c3.528,3.527 5.236,16.723 6.047,27.171l0.339,4.357l-8.715,0.678zm-4.032,-26.233l0,0l0,0z" fill="#9D3542" id="svg_450"/>
</g>
<path d="m30.622,55.47c0,0 -6.79,-0.266 -7.257,-0.224c-1.32,0.117 -3.098,0.479 -3.965,0.973c-1.958,1.114 -2.138,2.935 -2.305,4.379c-0.226,1.95 4.137,11.453 4.137,11.453l-1.692,8.038c0,0 -0.26,3.501 0,5.711l17.557,-2.327l-1.423,-6.738l-1.457,-8.822c0,0 2.514,-4.672 0.89,-6.394c-3.922,-4.16 -4.485,-6.049 -4.485,-6.049z" fill="#EDA956" id="svg_451"/>
<g id="svg_452">
<path d="m26.036,58.4l0,0c-1.314,0 -2.378,-1.065 -2.378,-2.379l0,-10.132c0,-0.157 0.127,-0.285 0.285,-0.285l4.188,0c0.157,0 0.285,0.127 0.285,0.285l0,10.132c-0.001,1.314 -1.066,2.379 -2.38,2.379z" fill="#E8A8B8" id="svg_453"/>
<path d="m23.658,51.307c0.63,1.633 2.003,4.149 4.757,4.768l0,-10.471l-4.3,0l-0.457,0.201l0,5.502z" fill="#D98E9D" id="svg_454" opacity="0.5"/>
</g>
<path d="m30.232,37.823l-1.413,0c-2.809,0 -5.086,2.277 -5.086,5.086l0,4.812c0,2.84 2.302,5.143 5.143,5.143l0.659,0c3.193,0 5.782,-2.589 5.782,-5.782l0,-4.173c0,-2.809 -2.277,-5.086 -5.085,-5.086z" fill="#E8A8B8" id="svg_455"/>
<path d="m25.836,46.607c0,1.511 -1.225,2.736 -2.736,2.736s-2.736,-1.225 -2.736,-2.736c0,-1.511 1.225,-2.736 2.736,-2.736s2.736,1.225 2.736,2.736z" fill="#E8A8B8" id="svg_456"/>
<path d="m23.798,42.265c0.213,0.985 0.35,1.986 0.41,2.991c0.114,-0.106 0.325,-0.155 0.47,-0.099" fill="none" id="svg_457"/>
<path d="m31.935,48.65l-2.962,-0.753c0,0 0.472,1.61 1.418,1.773c0.946,0.163 1.544,-1.02 1.544,-1.02z" fill="#FFFFFF" id="svg_458"/>
<circle cx="30.464" cy="44.299" fill="#3F3945" id="svg_459" r="0.552"/>
<path d="m34.877,44.299c0,0.305 -0.247,0.552 -0.552,0.552c-0.305,0 -0.552,-0.247 -0.552,-0.552c0,-0.305 0.247,-0.552 0.552,-0.552c0.305,0 0.552,0.247 0.552,0.552z" fill="#3F3945" id="svg_460"/>
<path d="m11.452,86.3c-0.717,-5.305 -2.256,-12.005 -2.459,-17.77l0,0l5.055,14.542c1.072,3.084 3.979,5.151 7.244,5.151l3.238,0c2.861,0 5.44,1.725 6.532,4.37l2.081,5.039l-11.007,0c0,-0.001 -9.065,0.647 -10.684,-11.332z" fill="#6F9EDE" id="svg_461"/>
<path d="m23.69,89.615c-0.835,-0.239 -1.695,-0.614 -2.165,-1.344c-0.47,-0.73 0.845,-1.456 1.633,-1.82c0.862,0.623 2.047,0.35 3.107,0.443c0.643,0.057 1.263,0.258 1.867,0.485c0.774,0.29 1.542,0.63 2.191,1.142c0.649,0.513 1.176,1.218 1.337,2.029c0.038,0.19 -0.093,0.338 -0.283,0.303c-0.19,-0.035 -0.324,-0.201 -0.449,-0.349c-0.393,-0.463 -0.874,-0.851 -1.41,-1.136c-0.097,-0.052 -0.21,0.105 -0.122,0.17c1.29,0.964 1.446,1.936 1.404,2.013c-0.046,0.083 -0.161,0.093 -0.254,0.073c-0.167,-0.037 -0.391,-0.344 -0.512,-0.464c-0.512,-0.51 -1.03,-0.69 -1.03,-0.69s-0.614,-0.087 0.722,0.964c0.164,0.129 0.329,0.293 0.351,0.5c0.004,0.037 0.003,0.075 -0.012,0.109c-0.017,0.041 -0.051,0.072 -0.088,0.096c-0.114,0.072 -0.262,0.074 -0.391,0.036c-0.129,-0.038 -0.245,-0.111 -0.359,-0.184c-0.558,-0.355 -1.116,-0.71 -1.675,-1.066c0.14,0.133 1.125,0.903 1.214,1.075c0.034,0.067 0.065,0.147 0.034,0.216c-0.044,0.102 -0.183,0.112 -0.294,0.101c-0.743,-0.075 -2.293,-1.017 -2.922,-1.421c-0.628,-0.404 -1.176,-1.076 -1.894,-1.281z" fill="#E8A8B8" id="svg_462"/>
<path d="m19.326,56.368c0,0 -7.823,5.529 -9.727,14.837c-1.904,9.307 4.138,16.479 11.694,17.018l2.888,-1.432c0,0 -9.08,-9.595 -8.445,-12.768c0.635,-3.173 5.921,-10.222 5.921,-10.222s-0.521,-8.173 -2.331,-7.433z" fill="#EDA956" id="svg_463"/>
</g>
<path d="m24.036,44.593c0,0 7.272,-1.157 8.422,-4.988c0,0 0.668,3.521 2.86,3.303c0,0 1.247,-7.694 -6.037,-7.37c-6.637,0.295 -7.568,3.739 -7.313,8.592c0,0.001 1.62,-0.817 2.068,0.463z" fill="#563238" id="svg_464"/>
<path d="m23.442,47.902c0,0.278 -0.226,0.504 -0.504,0.504c-0.278,0 -0.504,-0.226 -0.504,-0.504c0,-0.278 0.226,-0.504 0.504,-0.504c0.278,0 0.504,0.225 0.504,0.504z" fill="#FFFFFF" id="svg_465"/>
<path d="m56.561,44.817l2.687,-2.855c0,0 -0.068,-2.583 0.408,-4.01c0.476,-1.427 0.476,-1.427 0.476,-1.427s0.748,-0.136 0.68,0.952c-0.068,1.088 0.068,2.039 0.068,2.039s3.195,-5.709 4.01,-5.981c0.816,-0.272 -0.748,3.127 -0.952,3.602c-0.204,0.476 2.651,-3.058 2.991,-2.923c0.34,0.136 -0.884,2.515 -2.039,3.806c0,0 2.787,-2.447 3.127,-2.243c0.34,0.204 -1.555,2.214 -1.555,2.214s2.708,-2.37 0.671,0.776c-0.866,1.337 -4.962,4.554 -5.437,4.758c-0.476,0.204 -2.651,3.466 -2.651,3.466l-2.484,-2.174z" fill="#E8A8B8" id="svg_466"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 31 KiB

+157 -384
View File
@@ -1,406 +1,179 @@
<?xml version="1.0"?>
<svg width="180" height="140" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="BACKGROUND"/>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_5"/>
<g id="svg_20"/>
<g id="svg_35"/>
</g>
<g id="svg_48">
<g id="svg_51"/>
<g id="svg_56"/>
<g id="svg_70">
<g id="svg_74"/>
<g id="svg_76"/>
<g id="svg_78"/>
<g id="svg_80"/>
</g>
<g id="svg_82">
<g id="svg_86"/>
<g id="svg_88"/>
<g id="svg_90"/>
<g id="svg_92"/>
</g>
<g id="svg_95"/>
<g id="svg_100"/>
</g>
<g id="svg_104">
<g id="svg_116"/>
<g id="svg_118"/>
<g id="svg_126"/>
<g id="svg_135"/>
</g>
<g id="svg_146">
<g id="svg_155"/>
<g id="svg_157"/>
<g id="svg_165"/>
<g id="svg_175"/>
<g id="svg_180">
<g id="svg_184"/>
<g id="svg_186"/>
<g id="svg_188"/>
<g id="svg_190"/>
</g>
<g id="svg_192">
<g id="svg_196"/>
<g id="svg_198"/>
<g id="svg_200"/>
<g id="svg_202"/>
</g>
</g>
<g id="svg_205">
<g id="svg_206"/>
<g id="svg_209"/>
</g>
<g id="svg_213">
<g id="svg_214">
<g id="svg_216"/>
</g>
<g id="svg_219">
<g id="svg_221"/>
</g>
<g id="svg_224">
<g id="svg_226"/>
</g>
<g id="svg_228"/>
</g>
</g>
<g id="svg_231">
<?xml version="1.0" encoding="utf-8"?>
<svg width="180" height="140" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<g id="svg_232">
<g id="svg_233">
<g id="svg_234">
<path d="m76.933,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0,0 2.8,2.119 2.8,1.413z" fill="#2D294C" id="svg_235"/>
<path d="m81.83,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0.001,0 2.8,2.119 2.8,1.413z" fill="#2D294C" id="svg_236"/>
<g id="svg_233">
<g id="svg_234">
<path d="m76.933,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0,0 2.8,2.119 2.8,1.413z" fill="#2D294C" id="svg_235"/>
<path d="m81.83,96.939l-6.102,30.954l-0.712,0l4.014,-32.367c0.001,0 2.8,2.119 2.8,1.413z" fill="#2D294C" id="svg_236"/>
</g>
<g id="svg_237">
<path d="m98.866,96.939l6.102,30.954l0.712,0l-4.014,-32.367c-0.001,0 -2.8,2.119 -2.8,1.413z" fill="#2D294C" id="svg_238"/>
<path d="m93.968,96.939l6.102,30.954l0.712,0l-4.014,-32.367c0,0 -2.8,2.119 -2.8,1.413z" fill="#2D294C" id="svg_239"/>
</g>
</g>
<g id="svg_237">
<path d="m98.866,96.939l6.102,30.954l0.712,0l-4.014,-32.367c-0.001,0 -2.8,2.119 -2.8,1.413z" fill="#2D294C" id="svg_238"/>
<path d="m93.968,96.939l6.102,30.954l0.712,0l-4.014,-32.367c0,0 -2.8,2.119 -2.8,1.413z" fill="#2D294C" id="svg_239"/>
<path d="m103.771,77.118l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.748,0c-1.852,0 -3.353,1.501 -3.353,3.353l0,5.309c0,6.848 5.551,12.399 12.399,12.399l7.917,0l1.864,0l5.365,0c6.848,0 12.399,-5.551 12.399,-12.399l0,-5.309c0,-1.852 -1.501,-3.353 -3.353,-3.353l-0.746,0z" fill="#9292AA" id="svg_240"/>
<g id="svg_241">
<g id="svg_242">
<rect fill="#2D294C" height="30.178" id="svg_243" transform="matrix(0.374 -0.9274 0.9274 0.374 31.4581 419.478)" width="0.486" x="310.463764" y="-78.932512"/>
</g>
<g id="svg_244">
<rect fill="#2D294C" height="0.486" id="svg_245" transform="matrix(0.9271 -0.3748 0.3748 0.9271 -46.1495 135.573)" width="30.178" x="119.351398" y="23.748997"/>
</g>
</g>
</g>
<path d="m103.771,77.118l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.748,0c-1.852,0 -3.353,1.501 -3.353,3.353l0,5.309c0,6.848 5.551,12.399 12.399,12.399l7.917,0l1.864,0l5.365,0c6.848,0 12.399,-5.551 12.399,-12.399l0,-5.309c0,-1.852 -1.501,-3.353 -3.353,-3.353l-0.746,0z" fill="#9292AA" id="svg_240"/>
<g id="svg_241">
<g id="svg_242">
<rect fill="#2D294C" height="30.178" id="svg_243" transform="matrix(0.374 -0.9274 0.9274 0.374 31.4581 419.478)" width="0.486" x="310.463764" y="-78.932512"/>
</g>
<g id="svg_244">
<rect fill="#2D294C" height="0.486" id="svg_245" transform="matrix(0.9271 -0.3748 0.3748 0.9271 -46.1495 135.573)" width="30.178" x="119.351398" y="23.748997"/>
</g>
</g>
<path d="m103.942,77.118l-0.171,0l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.01,0l0,12.579c0,4.58 3.633,8.447 8.213,8.482c0.032,0 0.064,0 0.096,0l7.917,0l1.864,0l5.365,0c0.088,0 0.176,-0.001 0.264,-0.003c4.577,-0.096 8.206,-3.907 8.206,-8.485l0,-12.573z" fill="#CFCCDF" id="svg_246"/>
<path d="m70.573,93.419c2.27,2.894 5.79,4.76 9.753,4.76l7.917,0l1.864,0l5.365,0c3.963,0 7.483,-1.866 9.753,-4.76l-34.652,0z" fill="#9292AA" id="svg_247"/>
</g>
<g id="svg_248">
<g id="svg_249"/>
<g id="svg_251"/>
<g id="svg_253"/>
<g id="svg_255"/>
<g id="svg_260"/>
<g id="svg_264"/>
<g id="svg_268">
<g id="svg_269"/>
<g id="svg_271"/>
<g id="svg_273"/>
<g id="svg_275"/>
<g id="svg_277"/>
<g id="svg_279"/>
<g id="svg_281"/>
<g id="svg_283"/>
<g id="svg_285"/>
<g id="svg_287"/>
<g id="svg_289"/>
<g id="svg_291"/>
<g id="svg_293"/>
<g id="svg_295"/>
<g id="svg_297"/>
</g>
</g>
<g id="svg_299">
<g id="svg_300"/>
<g id="svg_308"/>
<g id="svg_313"/>
<g id="svg_315">
<g id="svg_319"/>
<g id="svg_321"/>
<g id="svg_323"/>
<g id="svg_325"/>
</g>
<g id="svg_329"/>
<path d="m103.942,77.118l-0.171,0l-3.711,-11.503c-1.121,-3.475 -4.355,-5.83 -8.007,-5.83l-1.946,0l-1.864,0l-4.498,0c-3.651,0 -6.886,2.355 -8.007,5.83l-3.711,11.503l-0.01,0l0,12.579c0,4.58 3.633,8.447 8.213,8.482c0.032,0 0.064,0 0.096,0l7.917,0l1.864,0l5.365,0c0.088,0 0.176,-0.001 0.264,-0.003c4.577,-0.096 8.206,-3.907 8.206,-8.485l0,-12.573z" fill="#CFCCDF" id="svg_246"/>
<path d="m70.573,93.419c2.27,2.894 5.79,4.76 9.753,4.76l7.917,0l1.864,0l5.365,0c3.963,0 7.483,-1.866 9.753,-4.76l-34.652,0z" fill="#9292AA" id="svg_247"/>
</g>
<g id="svg_345">
<polygon fill="#D7914E" id="svg_346" points="105.989990234375,118.74800109863281 106.02999877929688,125.25300598144531 106.30999755859375,127.28900146484375 109.94100952148438,127.48800659179688 110.94900512695312,126.00199890136719 110.30099487304688,117.8699951171875 "/>
<rect fill="#2D294C" height="15.55" id="svg_347" width="21.717" x="76.059" y="78.354"/>
<path d="m68.326,77.424c-0.902,-0.263 -1.782,-0.6 -2.629,-1.005c-1.383,-0.661 -2.848,-1.829 -2.702,-3.355c0.096,-1.006 0.995,-3.146 1.705,-3.865c1.915,-1.94 7.913,-13.124 9.945,-14.721c0.888,-0.698 9.498,-4.708 9.904,-2.435c0.077,0.43 -0.507,3.353 -0.629,3.772c-0.972,3.357 -3.12,4.862 -5.219,7.656c-1.819,2.422 -4.013,4.552 -5.728,7.048c0.632,1.036 1.411,2.12 2.385,2.843c3.033,2.25 6.071,4.491 9.116,6.724c-0.729,0.863 -1.156,1.903 -1.474,2.987c-5.004,-1.626 -9.621,-4.173 -14.674,-5.649z" fill="#D7914E" id="svg_348"/>
<path d="m103.921,77.424c0.902,-0.263 1.782,-0.6 2.629,-1.005c1.383,-0.661 2.848,-1.829 2.702,-3.355c-0.096,-1.006 -0.995,-3.146 -1.705,-3.865c-1.915,-1.94 -7.913,-13.124 -9.945,-14.721c-0.888,-0.698 -9.498,-4.708 -9.904,-2.435c-0.077,0.43 0.507,3.353 0.629,3.772c0.972,3.357 3.12,4.862 5.219,7.656c1.819,2.422 4.013,4.552 5.728,7.048c-0.632,1.036 -1.411,2.12 -2.385,2.843c-3.033,2.25 -6.071,4.491 -9.116,6.724c0.729,0.863 1.156,1.903 1.474,2.987c5.004,-1.626 9.621,-4.173 14.674,-5.649z" fill="#D7914E" id="svg_349"/>
<path d="m92.764,74.378c0,1.592 -1.348,2.882 -3.011,2.882c-1.663,0 -3.011,-1.29 -3.011,-2.882c0,-1.592 1.348,-2.882 3.011,-2.882c1.663,-0.001 3.011,1.29 3.011,2.882z" fill="#A0616B" id="svg_350"/>
<g id="svg_351">
<path d="m104.514,121.741l-0.079,-1.984c-0.668,-10.878 -2.243,-21.139 -3.588,-24.237c-1.649,-1.06 -7.638,-2.66 -13.028,-3.43l-4.808,-0.687l1.373,-9.615l4.808,0.687c4.352,0.622 14.889,2.484 18.744,6.338c1.096,1.095 4.427,1.511 6.193,30.349l-0.116,2.139l-9.499,0.44zm-3.804,-26.502l0,0l0,0z" fill="#2D294C" id="svg_352"/>
</g>
<g id="svg_353">
<path d="m75.533,124.821l-0.868,-4.778c-0.162,-0.892 -2.938,-16.203 -3.432,-23.825c-0.527,-8.127 4.433,-11.396 7.13,-11.99l4.743,-1.044l2.088,9.485l-4.083,0.899c-0.103,0.258 -0.261,0.86 -0.186,2.021c0.458,7.067 3.258,22.503 3.296,22.716l0.869,4.778l-9.557,1.738z" fill="#2D294C" id="svg_354"/>
</g>
<path d="m99.534,58.12c-0.233,-3.776 -5.585,-7.346 -12.204,-7.346c-6.619,0 -12.365,1.538 -13.534,5.16c-0.585,1.811 2.424,21.23 2.401,22.425c-0.075,3.845 -0.257,5.671 -0.257,5.671l21.993,0.187c0,0 0.083,-2.617 -0.09,-5.498c-0.11,-1.83 1.812,-18.636 1.691,-20.599z" fill="#D7914E" id="svg_355"/>
<path d="m86.871,53.229l-0.575,0c-1.537,0 -2.783,-1.246 -2.783,-2.783l0,-8.716l6.141,0l0,8.716c0,1.537 -1.246,2.783 -2.783,2.783z" fill="#EFAFC0" id="svg_356"/>
<path d="m83.514,44.073c0.813,2.108 2.586,5.356 6.14,6.155l0,-8.499l-5.55,0l-0.59,0.259l0,2.085z" fill="#D98E9D" id="svg_357" opacity="0.5"/>
<path d="m88.507,31.913l-1.4,0.188c-2.784,0.374 -4.738,2.933 -4.364,5.717l0.64,4.77c0.378,2.815 2.966,4.791 5.781,4.413l2.485,-0.334c2.154,-0.289 3.665,-2.269 3.376,-4.423l-0.801,-5.967c-0.373,-2.783 -2.933,-4.737 -5.717,-4.364z" fill="#EFAFC0" id="svg_358"/>
<path d="m85.319,41.204c0.201,1.498 -0.85,2.874 -2.348,3.075c-1.497,0.201 -2.874,-0.85 -3.075,-2.348c-0.201,-1.498 0.85,-2.874 2.348,-3.075c1.497,-0.201 2.874,0.85 3.075,2.348z" fill="#EFAFC0" id="svg_359"/>
<path d="m82.98,39.098c0.342,0.948 0.611,1.922 0.805,2.91c0.098,-0.121 0.301,-0.197 0.452,-0.161" fill="none" id="svg_360"/>
<path d="m79.405,36.266c0.064,0.203 0.171,0.389 0.277,0.573c0.448,0.779 0.895,1.558 1.343,2.337c0.533,-0.343 1.228,-0.251 1.862,-0.25c0.168,0.696 0.253,1.418 0.279,2.134c0.23,-0.047 0.468,-0.064 0.703,-0.052c-0.081,-0.824 -0.109,-1.66 -0.135,-2.487c0.207,-0.25 0.524,-0.439 0.753,-0.669c-0.368,-0.4 -0.571,-0.938 -0.615,-1.479c-0.056,-0.687 0.26,-1.236 0.6,-1.836c0.34,-0.6 0.843,-1.132 1.487,-1.379c1.383,-0.531 2.731,-1.006 4.207,-0.891c0.582,0.045 1.221,0.088 1.716,0.399c0.765,-0.017 1.343,-0.848 1.326,-1.613c-0.017,-0.765 -0.473,-1.453 -1.003,-2.004c-0.374,-0.389 -0.797,-0.736 -1.275,-0.987c-0.595,-0.313 -1.261,-0.47 -1.928,-0.554c-1.757,-0.221 -3.575,0.058 -5.185,0.796c0.294,0.06 0.505,0.162 0.799,0.222c-0.498,0.05 -0.985,0.172 -1.483,0.222c-0.53,0.053 -1.065,0.108 -1.57,0.277c-0.922,0.309 -1.687,0.985 -2.259,1.772c-0.572,0.787 -0.969,1.684 -1.345,2.581c-0.163,0.389 -0.326,0.803 -0.264,1.22c0.051,0.347 0.254,0.652 0.469,0.93c0.272,0.349 0.797,0.732 1.241,0.738z" fill="#2D294C" id="svg_361"/>
<path d="m90.769,42.967l-2.971,-0.714c0,0 0.493,1.603 1.441,1.755c0.947,0.15 1.53,-1.041 1.53,-1.041z" fill="#FFFFFF" id="svg_362"/>
<path d="m89.656,38.335c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.04 -0.58,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.302,-0.041 0.579,0.171 0.62,0.473z" fill="#2D294C" id="svg_363"/>
<path d="m93.483,37.821c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.041 -0.579,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.301,-0.041 0.579,0.171 0.62,0.473z" fill="#2D294C" id="svg_364"/>
<polygon fill="#FFFFFF" id="svg_365" points="86.84201049804688,52.28199768066406 82.7130126953125,51.47599792480469 83.10000610351562,54.055999755859375 87.48599243164062,60.11799621582031 91.87100219726562,54.82899475097656 89.93600463867188,51.08900451660156 "/>
<path d="m83.382,49.978c0,0 3.161,1.747 6.287,0l0.359,1.527l-0.773,3.712l-2.315,-2.559l-1.996,2.689l-2.102,-3.483l0.54,-1.886z" fill="#EFF2F7" id="svg_366"/>
<g id="svg_367">
<path d="m77.129,132.034c0,0 -0.359,1.321 -0.166,2.048c0.193,0.728 0.945,2.038 2.77,2.094c1.825,0.057 4.203,0.173 4.061,-4.926l-6.665,0.784z" fill="#3F3E66" id="svg_368"/>
<path d="m79.816,123.896c0.162,-0.288 -0.354,-2.445 1.152,-2.371c1.506,0.074 1.678,0.793 1.567,1.853c-0.111,1.06 -0.807,5.388 -0.807,5.388l-2.112,0.193l0.2,-5.063z" fill="#2D294C" id="svg_369"/>
<path d="m79.5,122.394c0,0 -0.642,3.727 -0.709,3.871c-0.067,0.145 -1.992,5.885 -1.714,6.789c0.2,0.651 1.325,1.583 3.732,1.492c1.852,-0.07 3.219,-1.763 2.945,-3.596c-0.243,-1.626 -0.549,-4.7 -0.547,-4.746c0.005,-0.089 -0.298,-3.866 -0.298,-3.866s-1.764,2.646 -1.904,5.904l-0.65,-0.035c-0.001,0.001 0.162,-5.996 -0.855,-5.813z" fill="#2D294C" id="svg_370"/>
<g id="svg_371">
<path d="m82.319,124.27c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.056 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.748,-0.955 2.831,0.148c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.045 -0.107,0.068 -0.168,0.068z" fill="#3F3E66" id="svg_372"/>
<polygon fill="#D7914E" id="svg_346" points="105.989990234375,118.74800109863281 106.02999877929688,125.25300598144531 106.30999755859375,127.28900146484375 109.94100952148438,127.48800659179688 110.94900512695312,126.00199890136719 110.30099487304688,117.8699951171875 "/>
<rect fill="#2D294C" height="15.55" id="svg_347" width="21.717" x="76.059" y="78.354"/>
<path d="m68.326,77.424c-0.902,-0.263 -1.782,-0.6 -2.629,-1.005c-1.383,-0.661 -2.848,-1.829 -2.702,-3.355c0.096,-1.006 0.995,-3.146 1.705,-3.865c1.915,-1.94 7.913,-13.124 9.945,-14.721c0.888,-0.698 9.498,-4.708 9.904,-2.435c0.077,0.43 -0.507,3.353 -0.629,3.772c-0.972,3.357 -3.12,4.862 -5.219,7.656c-1.819,2.422 -4.013,4.552 -5.728,7.048c0.632,1.036 1.411,2.12 2.385,2.843c3.033,2.25 6.071,4.491 9.116,6.724c-0.729,0.863 -1.156,1.903 -1.474,2.987c-5.004,-1.626 -9.621,-4.173 -14.674,-5.649z" fill="#D7914E" id="svg_348"/>
<path d="m103.921,77.424c0.902,-0.263 1.782,-0.6 2.629,-1.005c1.383,-0.661 2.848,-1.829 2.702,-3.355c-0.096,-1.006 -0.995,-3.146 -1.705,-3.865c-1.915,-1.94 -7.913,-13.124 -9.945,-14.721c-0.888,-0.698 -9.498,-4.708 -9.904,-2.435c-0.077,0.43 0.507,3.353 0.629,3.772c0.972,3.357 3.12,4.862 5.219,7.656c1.819,2.422 4.013,4.552 5.728,7.048c-0.632,1.036 -1.411,2.12 -2.385,2.843c-3.033,2.25 -6.071,4.491 -9.116,6.724c0.729,0.863 1.156,1.903 1.474,2.987c5.004,-1.626 9.621,-4.173 14.674,-5.649z" fill="#D7914E" id="svg_349"/>
<path d="m92.764,74.378c0,1.592 -1.348,2.882 -3.011,2.882c-1.663,0 -3.011,-1.29 -3.011,-2.882c0,-1.592 1.348,-2.882 3.011,-2.882c1.663,-0.001 3.011,1.29 3.011,2.882z" fill="#A0616B" id="svg_350"/>
<g id="svg_351">
<path d="m104.514,121.741l-0.079,-1.984c-0.668,-10.878 -2.243,-21.139 -3.588,-24.237c-1.649,-1.06 -7.638,-2.66 -13.028,-3.43l-4.808,-0.687l1.373,-9.615l4.808,0.687c4.352,0.622 14.889,2.484 18.744,6.338c1.096,1.095 4.427,1.511 6.193,30.349l-0.116,2.139l-9.499,0.44zm-3.804,-26.502l0,0l0,0z" fill="#2D294C" id="svg_352"/>
</g>
<g id="svg_373">
<path d="m82.161,125.502c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#3F3E66" id="svg_374"/>
<g id="svg_353">
<path d="m75.533,124.821l-0.868,-4.778c-0.162,-0.892 -2.938,-16.203 -3.432,-23.825c-0.527,-8.127 4.433,-11.396 7.13,-11.99l4.743,-1.044l2.088,9.485l-4.083,0.899c-0.103,0.258 -0.261,0.86 -0.186,2.021c0.458,7.067 3.258,22.503 3.296,22.716l0.869,4.778l-9.557,1.738z" fill="#2D294C" id="svg_354"/>
</g>
<g id="svg_375">
<path d="m82.003,126.735c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.037,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.102 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.091,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#3F3E66" id="svg_376"/>
<path d="m99.534,58.12c-0.233,-3.776 -5.585,-7.346 -12.204,-7.346c-6.619,0 -12.365,1.538 -13.534,5.16c-0.585,1.811 2.424,21.23 2.401,22.425c-0.075,3.845 -0.257,5.671 -0.257,5.671l21.993,0.187c0,0 0.083,-2.617 -0.09,-5.498c-0.11,-1.83 1.812,-18.636 1.691,-20.599z" fill="#D7914E" id="svg_355"/>
<path d="m86.871,53.229l-0.575,0c-1.537,0 -2.783,-1.246 -2.783,-2.783l0,-8.716l6.141,0l0,8.716c0,1.537 -1.246,2.783 -2.783,2.783z" fill="#EFAFC0" id="svg_356"/>
<path d="m83.514,44.073c0.813,2.108 2.586,5.356 6.14,6.155l0,-8.499l-5.55,0l-0.59,0.259l0,2.085z" fill="#D98E9D" id="svg_357" opacity="0.5"/>
<path d="m88.507,31.913l-1.4,0.188c-2.784,0.374 -4.738,2.933 -4.364,5.717l0.64,4.77c0.378,2.815 2.966,4.791 5.781,4.413l2.485,-0.334c2.154,-0.289 3.665,-2.269 3.376,-4.423l-0.801,-5.967c-0.373,-2.783 -2.933,-4.737 -5.717,-4.364z" fill="#EFAFC0" id="svg_358"/>
<path d="m85.319,41.204c0.201,1.498 -0.85,2.874 -2.348,3.075c-1.497,0.201 -2.874,-0.85 -3.075,-2.348c-0.201,-1.498 0.85,-2.874 2.348,-3.075c1.497,-0.201 2.874,0.85 3.075,2.348z" fill="#EFAFC0" id="svg_359"/>
<path d="m82.98,39.098c0.342,0.948 0.611,1.922 0.805,2.91c0.098,-0.121 0.301,-0.197 0.452,-0.161" fill="none" id="svg_360"/>
<path d="m79.405,36.266c0.064,0.203 0.171,0.389 0.277,0.573c0.448,0.779 0.895,1.558 1.343,2.337c0.533,-0.343 1.228,-0.251 1.862,-0.25c0.168,0.696 0.253,1.418 0.279,2.134c0.23,-0.047 0.468,-0.064 0.703,-0.052c-0.081,-0.824 -0.109,-1.66 -0.135,-2.487c0.207,-0.25 0.524,-0.439 0.753,-0.669c-0.368,-0.4 -0.571,-0.938 -0.615,-1.479c-0.056,-0.687 0.26,-1.236 0.6,-1.836c0.34,-0.6 0.843,-1.132 1.487,-1.379c1.383,-0.531 2.731,-1.006 4.207,-0.891c0.582,0.045 1.221,0.088 1.716,0.399c0.765,-0.017 1.343,-0.848 1.326,-1.613c-0.017,-0.765 -0.473,-1.453 -1.003,-2.004c-0.374,-0.389 -0.797,-0.736 -1.275,-0.987c-0.595,-0.313 -1.261,-0.47 -1.928,-0.554c-1.757,-0.221 -3.575,0.058 -5.185,0.796c0.294,0.06 0.505,0.162 0.799,0.222c-0.498,0.05 -0.985,0.172 -1.483,0.222c-0.53,0.053 -1.065,0.108 -1.57,0.277c-0.922,0.309 -1.687,0.985 -2.259,1.772c-0.572,0.787 -0.969,1.684 -1.345,2.581c-0.163,0.389 -0.326,0.803 -0.264,1.22c0.051,0.347 0.254,0.652 0.469,0.93c0.272,0.349 0.797,0.732 1.241,0.738z" fill="#2D294C" id="svg_361"/>
<path d="m90.769,42.967l-2.971,-0.714c0,0 0.493,1.603 1.441,1.755c0.947,0.15 1.53,-1.041 1.53,-1.041z" fill="#FFFFFF" id="svg_362"/>
<path d="m89.656,38.335c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.04 -0.58,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.302,-0.041 0.579,0.171 0.62,0.473z" fill="#2D294C" id="svg_363"/>
<path d="m93.483,37.821c0.04,0.302 -0.171,0.58 -0.473,0.62c-0.302,0.041 -0.579,-0.171 -0.62,-0.473c-0.04,-0.302 0.171,-0.58 0.473,-0.62c0.301,-0.041 0.579,0.171 0.62,0.473z" fill="#2D294C" id="svg_364"/>
<polygon fill="#FFFFFF" id="svg_365" points="86.84201049804688,52.28199768066406 82.7130126953125,51.47599792480469 83.10000610351562,54.055999755859375 87.48599243164062,60.11799621582031 91.87100219726562,54.82899475097656 89.93600463867188,51.08900451660156 "/>
<path d="m83.382,49.978c0,0 3.161,1.747 6.287,0l0.359,1.527l-0.773,3.712l-2.315,-2.559l-1.996,2.689l-2.102,-3.483l0.54,-1.886z" fill="#EFF2F7" id="svg_366"/>
<g id="svg_367">
<path d="m77.129,132.034c0,0 -0.359,1.321 -0.166,2.048c0.193,0.728 0.945,2.038 2.77,2.094c1.825,0.057 4.203,0.173 4.061,-4.926l-6.665,0.784z" fill="#3F3E66" id="svg_368"/>
<path d="m79.816,123.896c0.162,-0.288 -0.354,-2.445 1.152,-2.371c1.506,0.074 1.678,0.793 1.567,1.853c-0.111,1.06 -0.807,5.388 -0.807,5.388l-2.112,0.193l0.2,-5.063z" fill="#2D294C" id="svg_369"/>
<path d="m79.5,122.394c0,0 -0.642,3.727 -0.709,3.871c-0.067,0.145 -1.992,5.885 -1.714,6.789c0.2,0.651 1.325,1.583 3.732,1.492c1.852,-0.07 3.219,-1.763 2.945,-3.596c-0.243,-1.626 -0.549,-4.7 -0.547,-4.746c0.005,-0.089 -0.298,-3.866 -0.298,-3.866s-1.764,2.646 -1.904,5.904l-0.65,-0.035c-0.001,0.001 0.162,-5.996 -0.855,-5.813z" fill="#2D294C" id="svg_370"/>
<g id="svg_371">
<path d="m82.319,124.27c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.056 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.748,-0.955 2.831,0.148c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.045 -0.107,0.068 -0.168,0.068z" fill="#3F3E66" id="svg_372"/>
</g>
<g id="svg_373">
<path d="m82.161,125.502c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.339,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.506,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#3F3E66" id="svg_374"/>
</g>
<g id="svg_375">
<path d="m82.003,126.735c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.037,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.085 -0.252,0.073 -0.339,-0.028c-0.086,-0.102 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.091,0.247 -0.003,0.34c-0.046,0.046 -0.107,0.069 -0.168,0.069z" fill="#3F3E66" id="svg_376"/>
</g>
<g id="svg_377">
<path d="m81.845,127.968c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.338,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.048,0.046 -0.109,0.069 -0.169,0.069z" fill="#3F3E66" id="svg_378"/>
</g>
</g>
<g id="svg_377">
<path d="m81.845,127.968c-0.062,0 -0.124,-0.024 -0.172,-0.072c-1.036,-1.055 -2.13,-0.157 -2.176,-0.118c-0.101,0.086 -0.252,0.073 -0.338,-0.028c-0.086,-0.101 -0.074,-0.252 0.027,-0.338c0.505,-0.432 1.747,-0.956 2.831,0.147c0.093,0.095 0.092,0.247 -0.003,0.34c-0.048,0.046 -0.109,0.069 -0.169,0.069z" fill="#3F3E66" id="svg_378"/>
<g id="svg_379">
<path d="m109.648,124.917c0,0 -0.906,-1.496 -0.021,-1.749c0.885,-0.253 1.434,0.241 1.434,0.241l2.491,2.198l1.365,1.652l-1.831,-0.581l-3.438,-1.761z" fill="#2D294C" id="svg_380"/>
<path d="m106.055,124.579c0,0 0.944,1.669 1.662,1.261c0.406,-0.231 0.985,-1.755 1.573,-1.617c0.588,0.138 2.961,1.476 3.837,1.794c0.876,0.318 -2.173,-2.574 -2.173,-2.574s2.521,1.913 3.304,2.2c0.784,0.286 7.197,1.07 7.591,2.986l0.064,1.261c0,0 -1.789,2.128 -6.783,1.15c-4.995,-0.978 -7.744,-0.907 -8.65,-1.133c-0.907,-0.227 -1.107,-5.191 -0.425,-5.328z" fill="#2D294C" id="svg_381"/>
<path d="m116.03,130.259c-2.331,-0.074 -6.343,-1.231 -7.828,-1.202c-0.855,0.017 -1.737,-0.166 -2.331,-0.325c0.14,0.637 0.344,1.108 0.609,1.174c0.907,0.226 3.656,0.155 8.65,1.133c4.995,0.978 6.783,-1.15 6.783,-1.15l-0.064,-1.261c-0.423,1.066 -3.488,1.705 -5.819,1.631z" fill="#3F3E66" id="svg_382"/>
<g id="svg_383">
<path d="m111.068,126.016c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.032,0.086 -0.108,0.142 -0.192,0.153z" fill="#3F3E66" id="svg_384"/>
</g>
<g id="svg_385">
<path d="m111.893,126.603c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.296,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.878,1.073c-0.032,0.086 -0.107,0.142 -0.191,0.153z" fill="#3F3E66" id="svg_386"/>
</g>
<g id="svg_387">
<path d="m112.861,126.952c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.1 0.612,-0.058 0.908,0.124c0.113,0.069 0.149,0.217 0.079,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.108 -0.339,-0.133 -0.502,-0.078c-0.435,0.147 -0.781,0.824 -0.878,1.073c-0.032,0.085 -0.107,0.141 -0.191,0.152z" fill="#3F3E66" id="svg_388"/>
</g>
<g id="svg_389">
<path d="m113.865,127.252c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.103 0.611,-0.058 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.107 -0.339,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.033,0.086 -0.108,0.141 -0.192,0.153z" fill="#3F3E66" id="svg_390"/>
</g>
</g>
</g>
<g id="svg_379">
<path d="m109.648,124.917c0,0 -0.906,-1.496 -0.021,-1.749c0.885,-0.253 1.434,0.241 1.434,0.241l2.491,2.198l1.365,1.652l-1.831,-0.581l-3.438,-1.761z" fill="#2D294C" id="svg_380"/>
<path d="m106.055,124.579c0,0 0.944,1.669 1.662,1.261c0.406,-0.231 0.985,-1.755 1.573,-1.617c0.588,0.138 2.961,1.476 3.837,1.794c0.876,0.318 -2.173,-2.574 -2.173,-2.574s2.521,1.913 3.304,2.2c0.784,0.286 7.197,1.07 7.591,2.986l0.064,1.261c0,0 -1.789,2.128 -6.783,1.15c-4.995,-0.978 -7.744,-0.907 -8.65,-1.133c-0.907,-0.227 -1.107,-5.191 -0.425,-5.328z" fill="#2D294C" id="svg_381"/>
<path d="m116.03,130.259c-2.331,-0.074 -6.343,-1.231 -7.828,-1.202c-0.855,0.017 -1.737,-0.166 -2.331,-0.325c0.14,0.637 0.344,1.108 0.609,1.174c0.907,0.226 3.656,0.155 8.65,1.133c4.995,0.978 6.783,-1.15 6.783,-1.15l-0.064,-1.261c-0.423,1.066 -3.488,1.705 -5.819,1.631z" fill="#3F3E66" id="svg_382"/>
<g id="svg_383">
<path d="m111.068,126.016c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.032,0.086 -0.108,0.142 -0.192,0.153z" fill="#3F3E66" id="svg_384"/>
<path d="m85.292,83.099c-0.866,-0.069 -1.783,-0.265 -2.389,-0.888c-0.606,-0.622 0.539,-1.594 1.239,-2.108c0.968,0.44 2.076,-0.063 3.133,-0.182c0.641,-0.072 1.289,0.003 1.926,0.105c0.817,0.131 1.636,0.311 2.374,0.685c0.738,0.374 1.394,0.96 1.713,1.723c0.075,0.179 -0.024,0.35 -0.217,0.353c-0.194,0.003 -0.358,-0.133 -0.51,-0.253c-0.477,-0.376 -1.026,-0.66 -1.608,-0.834c-0.105,-0.031 -0.185,0.144 -0.086,0.191c1.456,0.689 1.801,1.611 1.775,1.694c-0.028,0.09 -0.139,0.124 -0.234,0.122c-0.171,-0.003 -0.452,-0.259 -0.594,-0.354c-0.603,-0.399 -1.146,-0.472 -1.146,-0.472s-0.619,0.036 0.899,0.801c0.187,0.094 0.381,0.221 0.443,0.421c0.011,0.035 0.017,0.073 0.01,0.109c-0.008,0.043 -0.035,0.081 -0.067,0.112c-0.098,0.093 -0.242,0.124 -0.376,0.112c-0.134,-0.011 -0.262,-0.06 -0.388,-0.109c-0.618,-0.237 -1.235,-0.475 -1.853,-0.712c0.163,0.103 1.282,0.662 1.403,0.813c0.047,0.059 0.092,0.131 0.077,0.204c-0.023,0.109 -0.157,0.146 -0.268,0.157c-0.743,0.074 -2.45,-0.542 -3.146,-0.813c-0.695,-0.269 -1.365,-0.818 -2.11,-0.877z" fill="#EFAFC0" id="svg_391"/>
<g id="svg_392">
<path d="m87.677,40.908c-1.249,0 -2.339,-0.926 -2.51,-2.197c-0.09,-0.671 0.086,-1.337 0.497,-1.875c0.411,-0.538 1.006,-0.884 1.677,-0.974c1.386,-0.186 2.663,0.79 2.849,2.174c0.186,1.385 -0.79,2.662 -2.175,2.849c-0.113,0.016 -0.226,0.023 -0.338,0.023zm0.003,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-0.606,0.081 -1.145,0.394 -1.516,0.88c-0.372,0.487 -0.531,1.089 -0.45,1.695c0.169,1.252 1.324,2.137 2.575,1.966c1.252,-0.168 2.134,-1.324 1.966,-2.576c-0.154,-1.148 -1.139,-1.986 -2.269,-1.986z" fill="#2D294C" id="svg_393"/>
</g>
<g id="svg_385">
<path d="m111.893,126.603c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.296,-0.102 0.611,-0.059 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.176,-0.107 -0.34,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.878,1.073c-0.032,0.086 -0.107,0.142 -0.191,0.153z" fill="#3F3E66" id="svg_386"/>
<g id="svg_394">
<path d="m93.067,40.184c-0.552,0 -1.086,-0.179 -1.532,-0.52c-0.538,-0.411 -0.884,-1.006 -0.974,-1.677c-0.186,-1.385 0.789,-2.663 2.174,-2.849c1.384,-0.184 2.663,0.79 2.849,2.175c0.09,0.671 -0.086,1.336 -0.497,1.874c-0.411,0.538 -1.006,0.884 -1.678,0.974c-0.114,0.015 -0.228,0.023 -0.342,0.023zm0.007,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-1.252,0.168 -2.133,1.324 -1.965,2.576c0.082,0.607 0.394,1.145 0.88,1.517c0.488,0.372 1.094,0.532 1.695,0.449c0.607,-0.081 1.146,-0.394 1.517,-0.88c0.372,-0.487 0.531,-1.088 0.45,-1.695c-0.156,-1.15 -1.141,-1.988 -2.271,-1.988z" fill="#2D294C" id="svg_395"/>
</g>
<g id="svg_387">
<path d="m112.861,126.952c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.1 0.612,-0.058 0.908,0.124c0.113,0.069 0.149,0.217 0.079,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.108 -0.339,-0.133 -0.502,-0.078c-0.435,0.147 -0.781,0.824 -0.878,1.073c-0.032,0.085 -0.107,0.141 -0.191,0.152z" fill="#3F3E66" id="svg_388"/>
<g id="svg_396">
<rect fill="#2D294C" height="0.242" id="svg_397" transform="matrix(0.9187 -0.3949 0.3949 0.9187 -20.6624 136.864)" width="2.505" x="133.483205" y="-48.057873"/>
</g>
<g id="svg_389">
<path d="m113.865,127.252c-0.039,0.005 -0.08,0.001 -0.119,-0.014c-0.124,-0.048 -0.186,-0.187 -0.138,-0.311c0.017,-0.045 0.434,-1.105 1.172,-1.355c0.297,-0.103 0.611,-0.058 0.908,0.123c0.113,0.069 0.149,0.217 0.08,0.33c-0.069,0.113 -0.217,0.149 -0.331,0.079c-0.175,-0.107 -0.339,-0.133 -0.503,-0.078c-0.437,0.148 -0.782,0.825 -0.877,1.073c-0.033,0.086 -0.108,0.141 -0.192,0.153z" fill="#3F3E66" id="svg_390"/>
<g id="svg_398">
<path d="m90.262,87.938l20.542,0l0,-1.506l-20.542,0c-0.416,0 -0.753,0.337 -0.753,0.753l0,0c0,0.415 0.337,0.753 0.753,0.753z" fill="#A8A5C4" id="svg_399"/>
<path d="m92.011,87.938l19.809,0c0.876,0 1.643,-0.59 1.867,-1.436l3.496,-13.172c0.325,-1.226 -0.599,-2.428 -1.867,-2.428l-17.297,0c-0.876,0 -1.643,0.589 -1.867,1.436l-4.141,15.6z" fill="#CFCCDF" id="svg_400"/>
<path d="m115.315,70.902c1.268,0 2.193,1.202 1.867,2.428l-3.496,13.172c-0.108,0.406 -0.342,0.751 -0.652,1.002l-12.041,-16.601l14.322,0l0,-0.001z" fill="#CFCCDF" id="svg_401"/>
<path d="m103.676,80.586c-0.217,0.859 0.324,1.556 1.208,1.556c0.884,0 1.776,-0.696 1.993,-1.556c0.217,-0.859 -0.324,-1.556 -1.208,-1.556c-0.884,0 -1.776,0.697 -1.993,1.556z" fill="#FFFFFF" id="svg_402"/>
</g>
</g>
<path d="m85.292,83.099c-0.866,-0.069 -1.783,-0.265 -2.389,-0.888c-0.606,-0.622 0.539,-1.594 1.239,-2.108c0.968,0.44 2.076,-0.063 3.133,-0.182c0.641,-0.072 1.289,0.003 1.926,0.105c0.817,0.131 1.636,0.311 2.374,0.685c0.738,0.374 1.394,0.96 1.713,1.723c0.075,0.179 -0.024,0.35 -0.217,0.353c-0.194,0.003 -0.358,-0.133 -0.51,-0.253c-0.477,-0.376 -1.026,-0.66 -1.608,-0.834c-0.105,-0.031 -0.185,0.144 -0.086,0.191c1.456,0.689 1.801,1.611 1.775,1.694c-0.028,0.09 -0.139,0.124 -0.234,0.122c-0.171,-0.003 -0.452,-0.259 -0.594,-0.354c-0.603,-0.399 -1.146,-0.472 -1.146,-0.472s-0.619,0.036 0.899,0.801c0.187,0.094 0.381,0.221 0.443,0.421c0.011,0.035 0.017,0.073 0.01,0.109c-0.008,0.043 -0.035,0.081 -0.067,0.112c-0.098,0.093 -0.242,0.124 -0.376,0.112c-0.134,-0.011 -0.262,-0.06 -0.388,-0.109c-0.618,-0.237 -1.235,-0.475 -1.853,-0.712c0.163,0.103 1.282,0.662 1.403,0.813c0.047,0.059 0.092,0.131 0.077,0.204c-0.023,0.109 -0.157,0.146 -0.268,0.157c-0.743,0.074 -2.45,-0.542 -3.146,-0.813c-0.695,-0.269 -1.365,-0.818 -2.11,-0.877z" fill="#EFAFC0" id="svg_391"/>
<g id="svg_392">
<path d="m87.677,40.908c-1.249,0 -2.339,-0.926 -2.51,-2.197c-0.09,-0.671 0.086,-1.337 0.497,-1.875c0.411,-0.538 1.006,-0.884 1.677,-0.974c1.386,-0.186 2.663,0.79 2.849,2.174c0.186,1.385 -0.79,2.662 -2.175,2.849c-0.113,0.016 -0.226,0.023 -0.338,0.023zm0.003,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-0.606,0.081 -1.145,0.394 -1.516,0.88c-0.372,0.487 -0.531,1.089 -0.45,1.695c0.169,1.252 1.324,2.137 2.575,1.966c1.252,-0.168 2.134,-1.324 1.966,-2.576c-0.154,-1.148 -1.139,-1.986 -2.269,-1.986z" fill="#2D294C" id="svg_393"/>
</g>
<g id="svg_394">
<path d="m93.067,40.184c-0.552,0 -1.086,-0.179 -1.532,-0.52c-0.538,-0.411 -0.884,-1.006 -0.974,-1.677c-0.186,-1.385 0.789,-2.663 2.174,-2.849c1.384,-0.184 2.663,0.79 2.849,2.175c0.09,0.671 -0.086,1.336 -0.497,1.874c-0.411,0.538 -1.006,0.884 -1.678,0.974c-0.114,0.015 -0.228,0.023 -0.342,0.023zm0.007,-4.826c-0.102,0 -0.203,0.007 -0.306,0.021c-1.252,0.168 -2.133,1.324 -1.965,2.576c0.082,0.607 0.394,1.145 0.88,1.517c0.488,0.372 1.094,0.532 1.695,0.449c0.607,-0.081 1.146,-0.394 1.517,-0.88c0.372,-0.487 0.531,-1.088 0.45,-1.695c-0.156,-1.15 -1.141,-1.988 -2.271,-1.988z" fill="#2D294C" id="svg_395"/>
</g>
<g id="svg_396">
<rect fill="#2D294C" height="0.242" id="svg_397" transform="matrix(0.9187 -0.3949 0.3949 0.9187 -20.6624 136.864)" width="2.505" x="133.483205" y="-48.057873"/>
</g>
<g id="svg_398">
<path d="m90.262,87.938l20.542,0l0,-1.506l-20.542,0c-0.416,0 -0.753,0.337 -0.753,0.753l0,0c0,0.415 0.337,0.753 0.753,0.753z" fill="#A8A5C4" id="svg_399"/>
<path d="m92.011,87.938l19.809,0c0.876,0 1.643,-0.59 1.867,-1.436l3.496,-13.172c0.325,-1.226 -0.599,-2.428 -1.867,-2.428l-17.297,0c-0.876,0 -1.643,0.589 -1.867,1.436l-4.141,15.6z" fill="#CFCCDF" id="svg_400"/>
<path d="m115.315,70.902c1.268,0 2.193,1.202 1.867,2.428l-3.496,13.172c-0.108,0.406 -0.342,0.751 -0.652,1.002l-12.041,-16.601l14.322,0l0,-0.001z" fill="#CFCCDF" id="svg_401"/>
<path d="m103.676,80.586c-0.217,0.859 0.324,1.556 1.208,1.556c0.884,0 1.776,-0.696 1.993,-1.556c0.217,-0.859 -0.324,-1.556 -1.208,-1.556c-0.884,0 -1.776,0.697 -1.993,1.556z" fill="#FFFFFF" id="svg_402"/>
</g>
</g>
<g id="svg_403">
<path d="m126.823,42.216l-2.049,-3.268c0,0 0.64,-1.378 -0.06,-2.56c-0.699,-1.182 -1.769,-3.318 -2.053,-3.046s0.287,2.342 0.287,2.342s-1.883,-3.209 -3.159,-3.401c-1.276,-0.193 1.325,3.309 1.325,3.309s-2.943,-3.063 -3.122,-2.455s1.769,3.318 1.933,3.488c0.164,0.17 -2.237,-2.215 -2.411,-1.829c-0.175,0.386 2.694,4.394 3.295,4.906c0.602,0.513 5.102,3.607 5.102,3.607l0.912,-1.093z" fill="#EFAFC0" id="svg_404"/>
<path d="m152.166,57.969l-0.035,-0.084l-9.44,-2.281l-1.141,4.72c-0.16,0.665 -4.486,17.513 -4.967,25.197c-0.286,4.586 11.326,34.167 12.018,35.98l0.099,1.43l-1.833,2.344c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.9,-1.025 2.9,-1.025l-1.123,-4.079c0,0 -3.448,-21.082 -4.531,-27.574c-0.438,-2.627 -1.976,-4.794 -1.954,-5.541c0.256,-4.092 3.397,-11.984 5.887,-17.698c-0.193,5.235 -0.476,12.31 -0.735,16.441c-0.282,4.514 4.782,36.055 4.784,37.293l0.006,-0.012l-0.008,0.041l-2.354,2.58c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.573,-1.911 2.573,-1.911s1.925,-36.981 2.142,-40.473c0.463,-7.413 0.469,-20.196 0.469,-20.737l0,-4.856l-8.889,0l0,0.001z" fill="#EFAFC0" id="svg_405"/>
<path d="m141.222,34.996c-0.081,1.39 0.046,2.794 -0.178,4.168c-0.224,1.374 -0.881,2.774 -2.104,3.438c-1.778,0.965 -3.985,0.055 -5.662,-1.077c-1.677,-1.132 -3.352,-2.546 -5.374,-2.628c-2.174,-0.088 -4.267,1.672 -4.554,3.829c-0.278,2.09 1.001,4.141 2.706,5.38c1.706,1.239 3.786,1.824 5.818,2.387c2.631,0.728 5.511,1.443 8.015,0.354c1.623,-0.706 2.845,-2.08 3.982,-3.437c2.616,-3.121 5.148,-6.53 5.932,-10.527c0.536,-2.731 0.007,-8.52 -3.222,-9.671c-3.815,-1.359 -5.207,5.17 -5.359,7.784z" fill="#5387C4" id="svg_406"/>
<path d="m163.17,56.037l0.452,10.385l-2.032,17.61l-25.06,-1.355l0.249,-2.57c0.734,-7.587 2.437,-15.048 5.067,-22.202l0.846,-2.301l20.478,0.433z" fill="#DC614B" id="svg_407"/>
<path d="m166.608,27.766c-1.695,-1.277 -2.558,-3.404 -2.871,-5.503c-0.313,-2.1 -0.152,-4.237 -0.217,-6.359c-0.046,-1.511 -0.218,-3.057 -0.898,-4.407c-0.68,-1.35 -1.955,-2.479 -3.46,-2.629c-0.924,-0.092 -1.869,0.183 -2.779,-0.006c-0.198,0.405 -0.265,0.944 0.064,1.251c0.33,0.307 0.747,0.518 1.037,0.863c0.396,0.469 0.501,1.116 0.519,1.729c0.082,2.845 -1.37,5.539 -1.477,8.384c-0.052,1.381 0.227,2.787 0.901,3.993c0.674,1.206 1.758,2.199 3.058,2.666c1.841,0.659 4.217,0.449 6.123,0.018z" fill="#2D294C" id="svg_408"/>
<path d="m148.76,25.747l1.141,-0.145c0,0 1.821,1.121 2.287,1.037c1.09,-0.195 1.289,-0.589 1.289,-0.589s1.077,-0.719 1.563,-0.685c6.145,0.436 5.828,4.114 6.358,6.234c0.928,3.716 -2.395,9.115 -2.395,9.115s4.126,13.236 3.942,15.548l-11.556,1.9c-3.17,0.521 -6.421,-0.151 -9.124,-1.887l0,0l1.264,-11.978l-0.833,-2.272c-1.126,-3.07 -1.414,-6.387 -0.821,-9.603c0.253,-1.368 0.523,-2.637 0.751,-3.252c0.644,-1.736 4.295,-3.189 6.134,-3.423z" fill="#6F9EDE" id="svg_409"/>
<g id="svg_410">
<g id="svg_411">
<path d="m152.026,28.777l0,0c1.696,0 3.07,-1.375 3.07,-3.07l0,-8.061c0,-0.203 -0.165,-0.367 -0.367,-0.367l-5.406,0c-0.203,0 -0.367,0.164 -0.367,0.367l0,8.061c-0.001,1.695 1.374,3.07 3.07,3.07z" fill="#EFAFC0" id="svg_412"/>
<path d="m155.096,19.621c-0.813,2.108 -2.586,5.356 -6.141,6.155l0,-8.499l5.55,0l0.59,0.259l0,2.085l0.001,0z" fill="#D98E9D" id="svg_413" opacity="0.5"/>
<path d="m126.823,42.216l-2.049,-3.268c0,0 0.64,-1.378 -0.06,-2.56c-0.699,-1.182 -1.769,-3.318 -2.053,-3.046s0.287,2.342 0.287,2.342s-1.883,-3.209 -3.159,-3.401c-1.276,-0.193 1.325,3.309 1.325,3.309s-2.943,-3.063 -3.122,-2.455s1.769,3.318 1.933,3.488c0.164,0.17 -2.237,-2.215 -2.411,-1.829c-0.175,0.386 2.694,4.394 3.295,4.906c0.602,0.513 5.102,3.607 5.102,3.607l0.912,-1.093z" fill="#EFAFC0" id="svg_404"/>
<path d="m152.166,57.969l-0.035,-0.084l-9.44,-2.281l-1.141,4.72c-0.16,0.665 -4.486,17.513 -4.967,25.197c-0.286,4.586 11.326,34.167 12.018,35.98l0.099,1.43l-1.833,2.344c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.9,-1.025 2.9,-1.025l-1.123,-4.079c0,0 -3.448,-21.082 -4.531,-27.574c-0.438,-2.627 -1.976,-4.794 -1.954,-5.541c0.256,-4.092 3.397,-11.984 5.887,-17.698c-0.193,5.235 -0.476,12.31 -0.735,16.441c-0.282,4.514 4.782,36.055 4.784,37.293l0.006,-0.012l-0.008,0.041l-2.354,2.58c0,0 1.511,0.448 3.066,0.122c1.555,-0.326 2.573,-1.911 2.573,-1.911s1.925,-36.981 2.142,-40.473c0.463,-7.413 0.469,-20.196 0.469,-20.737l0,-4.856l-8.889,0l0,0.001z" fill="#EFAFC0" id="svg_405"/>
<path d="m141.222,34.996c-0.081,1.39 0.046,2.794 -0.178,4.168c-0.224,1.374 -0.881,2.774 -2.104,3.438c-1.778,0.965 -3.985,0.055 -5.662,-1.077c-1.677,-1.132 -3.352,-2.546 -5.374,-2.628c-2.174,-0.088 -4.267,1.672 -4.554,3.829c-0.278,2.09 1.001,4.141 2.706,5.38c1.706,1.239 3.786,1.824 5.818,2.387c2.631,0.728 5.511,1.443 8.015,0.354c1.623,-0.706 2.845,-2.08 3.982,-3.437c2.616,-3.121 5.148,-6.53 5.932,-10.527c0.536,-2.731 0.007,-8.52 -3.222,-9.671c-3.815,-1.359 -5.207,5.17 -5.359,7.784z" fill="#6F9EDE" id="svg_406"/>
<path d="m163.17,56.037l0.452,10.385l-2.032,17.61l-25.06,-1.355l0.249,-2.57c0.734,-7.587 2.437,-15.048 5.067,-22.202l0.846,-2.301l20.478,0.433z" fill="#DC614B" id="svg_407"/>
<path d="m166.608,27.766c-1.695,-1.277 -2.558,-3.404 -2.871,-5.503c-0.313,-2.1 -0.152,-4.237 -0.217,-6.359c-0.046,-1.511 -0.218,-3.057 -0.898,-4.407c-0.68,-1.35 -1.955,-2.479 -3.46,-2.629c-0.924,-0.092 -1.869,0.183 -2.779,-0.006c-0.198,0.405 -0.265,0.944 0.064,1.251c0.33,0.307 0.747,0.518 1.037,0.863c0.396,0.469 0.501,1.116 0.519,1.729c0.082,2.845 -1.37,5.539 -1.477,8.384c-0.052,1.381 0.227,2.787 0.901,3.993c0.674,1.206 1.758,2.199 3.058,2.666c1.841,0.659 4.217,0.449 6.123,0.018z" fill="#2D294C" id="svg_408"/>
<path d="m148.76,25.747l1.141,-0.145c0,0 1.821,1.121 2.287,1.037c1.09,-0.195 1.289,-0.589 1.289,-0.589s1.077,-0.719 1.563,-0.685c6.145,0.436 5.828,4.114 6.358,6.234c0.928,3.716 -2.395,9.115 -2.395,9.115s4.126,13.236 3.942,15.548l-11.556,1.9c-3.17,0.521 -6.421,-0.151 -9.124,-1.887l0,0l1.264,-11.978l-0.833,-2.272c-1.126,-3.07 -1.414,-6.387 -0.821,-9.603c0.253,-1.368 0.523,-2.637 0.751,-3.252c0.644,-1.736 4.295,-3.189 6.134,-3.423z" fill="#6F9EDE" id="svg_409"/>
<g id="svg_410">
<g id="svg_411">
<path d="m152.026,28.777l0,0c1.696,0 3.07,-1.375 3.07,-3.07l0,-8.061c0,-0.203 -0.165,-0.367 -0.367,-0.367l-5.406,0c-0.203,0 -0.367,0.164 -0.367,0.367l0,8.061c-0.001,1.695 1.374,3.07 3.07,3.07z" fill="#EFAFC0" id="svg_412"/>
<path d="m155.096,19.621c-0.813,2.108 -2.586,5.356 -6.141,6.155l0,-8.499l5.55,0l0.59,0.259l0,2.085l0.001,0z" fill="#D98E9D" id="svg_413" opacity="0.5"/>
</g>
<path d="m147.55,8.2l1.413,0c2.809,0 5.086,2.277 5.086,5.086l0,4.812c0,2.84 -2.302,5.143 -5.143,5.143l-2.507,0c-2.173,0 -3.934,-1.761 -3.934,-3.934l0,-6.021c0,-2.809 2.277,-5.086 5.085,-5.086z" fill="#EFAFC0" id="svg_414"/>
<circle cx="154.682" cy="16.984" fill="#EFAFC0" id="svg_415" r="2.736"/>
<path d="m153.984,14.586c-0.213,0.985 -0.35,1.986 -0.41,2.991c-0.114,-0.107 -0.325,-0.155 -0.47,-0.099" fill="none" id="svg_416"/>
<path d="m145.848,19.027l2.962,-0.753c0,0 -0.472,1.61 -1.418,1.773c-0.947,0.163 -1.544,-1.02 -1.544,-1.02z" fill="#FFFFFF" id="svg_417"/>
<path d="m146.767,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#2D294C" id="svg_418"/>
<path d="m142.905,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#2D294C" id="svg_419"/>
<path d="m151.485,8.973c0,0 -1.112,2.88 0.312,4.687c0.434,0.551 0.784,2.299 0.931,2.984c0.153,0 0.321,-1.654 1.2,-2.207c0.304,-0.195 1.535,-0.114 1.535,-0.114s0.673,-1.691 0.818,-2.126c0.185,-0.554 0.492,-1.063 0.666,-1.62c0.581,-1.859 -0.652,-4.078 -2.537,-4.567c-0.551,-0.143 -1.122,-0.216 -1.691,-0.236c-12.074,-0.429 -10.65,5.234 -10.605,6.527c5.007,0.668 9.371,-3.328 9.371,-3.328z" fill="#2D294C" id="svg_420"/>
</g>
<path d="m147.55,8.2l1.413,0c2.809,0 5.086,2.277 5.086,5.086l0,4.812c0,2.84 -2.302,5.143 -5.143,5.143l-2.507,0c-2.173,0 -3.934,-1.761 -3.934,-3.934l0,-6.021c0,-2.809 2.277,-5.086 5.085,-5.086z" fill="#EFAFC0" id="svg_414"/>
<circle cx="154.682" cy="16.984" fill="#EFAFC0" id="svg_415" r="2.736"/>
<path d="m153.984,14.586c-0.213,0.985 -0.35,1.986 -0.41,2.991c-0.114,-0.107 -0.325,-0.155 -0.47,-0.099" fill="none" id="svg_416"/>
<path d="m145.848,19.027l2.962,-0.753c0,0 -0.472,1.61 -1.418,1.773c-0.947,0.163 -1.544,-1.02 -1.544,-1.02z" fill="#FFFFFF" id="svg_417"/>
<path d="m146.767,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#2D294C" id="svg_418"/>
<path d="m142.905,14.676c0,0.305 0.247,0.552 0.552,0.552c0.305,0 0.552,-0.247 0.552,-0.552c0,-0.305 -0.247,-0.552 -0.552,-0.552c-0.305,0 -0.552,0.247 -0.552,0.552z" fill="#2D294C" id="svg_419"/>
<path d="m151.485,8.973c0,0 -1.112,2.88 0.312,4.687c0.434,0.551 0.784,2.299 0.931,2.984c0.153,0 0.321,-1.654 1.2,-2.207c0.304,-0.195 1.535,-0.114 1.535,-0.114s0.673,-1.691 0.818,-2.126c0.185,-0.554 0.492,-1.063 0.666,-1.62c0.581,-1.859 -0.652,-4.078 -2.537,-4.567c-0.551,-0.143 -1.122,-0.216 -1.691,-0.236c-12.074,-0.429 -10.65,5.234 -10.605,6.527c5.007,0.668 9.371,-3.328 9.371,-3.328z" fill="#2D294C" id="svg_420"/>
</g>
<path d="m163.38,57.685c0.802,-0.333 1.613,-0.804 1.996,-1.584c0.383,-0.78 -1.006,-1.349 -1.831,-1.621c-0.785,0.718 -1.993,0.582 -3.035,0.797c-0.632,0.13 -1.225,0.402 -1.799,0.696c-0.736,0.377 -1.46,0.802 -2.046,1.386c-0.586,0.584 -1.028,1.344 -1.095,2.169c-0.016,0.193 0.131,0.325 0.316,0.269c0.185,-0.057 0.299,-0.237 0.407,-0.398c0.337,-0.505 0.771,-0.945 1.27,-1.29c0.091,-0.063 0.221,0.08 0.141,0.155c-1.171,1.105 -1.214,2.089 -1.164,2.16c0.055,0.077 0.171,0.074 0.26,0.044c0.161,-0.056 0.349,-0.387 0.455,-0.52c0.45,-0.566 0.944,-0.804 0.944,-0.804s0.6,-0.157 -0.607,1.04c-0.148,0.147 -0.293,0.329 -0.291,0.538c0,0.037 0.006,0.074 0.024,0.106c0.021,0.038 0.059,0.066 0.098,0.085c0.122,0.058 0.268,0.043 0.392,-0.009c0.124,-0.052 0.231,-0.139 0.335,-0.223c0.514,-0.417 1.027,-0.834 1.541,-1.251c-0.123,0.149 -1.014,1.026 -1.082,1.207c-0.026,0.07 -0.047,0.153 -0.009,0.218c0.056,0.096 0.195,0.09 0.303,0.066c0.73,-0.16 2.161,-1.274 2.74,-1.746c0.58,-0.473 1.047,-1.203 1.737,-1.49z" fill="#EFAFC0" id="svg_421"/>
<path d="m163.052,29.686c2.636,2.75 5.273,5.5 7.909,8.25c2.443,2.548 5.045,5.473 5.038,9.003c-0.005,2.685 -1.557,5.122 -3.318,7.148c-1.249,1.437 -2.708,2.804 -4.535,3.338c-1.827,0.535 -4.086,-0.005 -5.022,-1.663c-1.015,-1.796 -0.176,-4.044 0.782,-5.871c0.958,-1.827 2.065,-3.848 1.519,-5.837c-0.388,-1.416 -1.538,-2.469 -2.585,-3.498c-2.957,-2.908 -5.618,-6.529 -6.459,-10.668c-0.357,-1.759 -0.916,-3.975 1.325,-3.962c1.979,0.012 4.113,2.473 5.346,3.76z" fill="#6F9EDE" id="svg_422"/>
<path d="m148.515,121.53l-1.375,3.425l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l11.019,-0.762l-1.007,-6.275l-3.829,0.317z" fill="#2D294C" id="svg_423"/>
<path d="m152.805,125.823l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l10.017,-0.755l0.776,-6.717l-3.943,-0.403l0.317,1.9l-2.359,2.68z" fill="#2D294C" id="svg_424"/>
<path d="m163.38,57.685c0.802,-0.333 1.613,-0.804 1.996,-1.584c0.383,-0.78 -1.006,-1.349 -1.831,-1.621c-0.785,0.718 -1.993,0.582 -3.035,0.797c-0.632,0.13 -1.225,0.402 -1.799,0.696c-0.736,0.377 -1.46,0.802 -2.046,1.386c-0.586,0.584 -1.028,1.344 -1.095,2.169c-0.016,0.193 0.131,0.325 0.316,0.269c0.185,-0.057 0.299,-0.237 0.407,-0.398c0.337,-0.505 0.771,-0.945 1.27,-1.29c0.091,-0.063 0.221,0.08 0.141,0.155c-1.171,1.105 -1.214,2.089 -1.164,2.16c0.055,0.077 0.171,0.074 0.26,0.044c0.161,-0.056 0.349,-0.387 0.455,-0.52c0.45,-0.566 0.944,-0.804 0.944,-0.804s0.6,-0.157 -0.607,1.04c-0.148,0.147 -0.293,0.329 -0.291,0.538c0,0.037 0.006,0.074 0.024,0.106c0.021,0.038 0.059,0.066 0.098,0.085c0.122,0.058 0.268,0.043 0.392,-0.009c0.124,-0.052 0.231,-0.139 0.335,-0.223c0.514,-0.417 1.027,-0.834 1.541,-1.251c-0.123,0.149 -1.014,1.026 -1.082,1.207c-0.026,0.07 -0.047,0.153 -0.009,0.218c0.056,0.096 0.195,0.09 0.303,0.066c0.73,-0.16 2.161,-1.274 2.74,-1.746c0.58,-0.473 1.047,-1.203 1.737,-1.49z" fill="#EFAFC0" id="svg_421"/>
<path d="m163.052,29.686c2.636,2.75 5.273,5.5 7.909,8.25c2.443,2.548 5.045,5.473 5.038,9.003c-0.005,2.685 -1.557,5.122 -3.318,7.148c-1.249,1.437 -2.708,2.804 -4.535,3.338c-1.827,0.535 -4.086,-0.005 -5.022,-1.663c-1.015,-1.796 -0.176,-4.044 0.782,-5.871c0.958,-1.827 2.065,-3.848 1.519,-5.837c-0.388,-1.416 -1.538,-2.469 -2.585,-3.498c-2.957,-2.908 -5.618,-6.529 -6.459,-10.668c-0.357,-1.759 -0.916,-3.975 1.325,-3.962c1.979,0.012 4.113,2.473 5.346,3.76z" fill="#6F9EDE" id="svg_422"/>
<path d="m148.515,121.53l-1.375,3.425l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l11.019,-0.762l-1.007,-6.275l-3.829,0.317z" fill="#2D294C" id="svg_423"/>
<path d="m152.805,125.823l-3.653,1.661c-1.819,0.738 -1.155,1.634 -1.155,1.634l10.017,-0.755l0.776,-6.717l-3.943,-0.403l0.317,1.9l-2.359,2.68z" fill="#2D294C" id="svg_424"/>
</g>
<g id="svg_425">
<path d="m24.854,55.079c4.549,0.076 9.099,0.153 13.648,0.229c1.546,0.026 3.103,0.051 4.624,-0.227c3.221,-0.588 11.197,-8.695 13.435,-10.265c0.838,0.743 1.646,1.432 2.484,2.174c-2.347,4.501 -3.586,7.027 -6.704,11.032c-0.932,1.197 -1.938,2.402 -3.293,3.082c-1.054,0.528 -2.246,0.7 -3.417,0.839c-4.995,0.59 -10.047,0.696 -15.063,0.317c-0.661,-0.05 -1.359,-0.122 -1.888,-0.522c-0.461,-0.348 -0.723,-0.894 -0.967,-1.418c-1.162,-2.485 -1.699,-2.754 -2.859,-5.241z" fill="#EDA956" id="svg_426"/>
<path d="m27.581,35.701c0,0 -7.203,0.647 -9.146,7.365c-1.943,6.718 1.781,9.308 1.781,9.308l1.376,0.081l0.648,-1.295l0.486,1.295l8.579,0.971l0.567,-0.648l0.405,0.648c0,0 4.209,-1.295 3.966,-4.694c-0.243,-3.399 -0.081,-5.018 -1.133,-7.041c-1.052,-2.023 -2.914,-4.452 -2.914,-4.452l-4.615,-1.538z" fill="#563238" id="svg_427"/>
<g id="svg_428">
<path d="m35.733,93.744l4.374,34.965l0.712,0l-2.287,-36.243c0.001,0.001 -2.799,1.917 -2.799,1.278z" fill="#2D294C" id="svg_429"/>
<path d="m45.446,92.449l16.677,36.26l0.712,0l-14.589,-37.538c0,0.001 -2.8,1.917 -2.8,1.278z" fill="#2D294C" id="svg_430"/>
<path d="m23.661,92.449l-16.677,36.26l-0.712,0l14.589,-37.538c0.001,0.001 2.8,1.917 2.8,1.278z" fill="#2D294C" id="svg_431"/>
<path d="m31.755,93.744l-4.374,34.965l-0.712,0l2.287,-36.243c-0.001,0.001 2.799,1.917 2.799,1.278z" fill="#2D294C" id="svg_432"/>
<g id="svg_433">
<rect fill="#2D294C" height="0.485" id="svg_434" transform="matrix(0.4675 -0.884 0.884 0.4675 -9.1243 344.263)" width="19.406" x="230.048508" y="-67.858469"/>
<path d="m24.854,55.079c4.549,0.076 9.099,0.153 13.648,0.229c1.546,0.026 3.103,0.051 4.624,-0.227c3.221,-0.588 11.197,-8.695 13.435,-10.265c0.838,0.743 1.646,1.432 2.484,2.174c-2.347,4.501 -3.586,7.027 -6.704,11.032c-0.932,1.197 -1.938,2.402 -3.293,3.082c-1.054,0.528 -2.246,0.7 -3.417,0.839c-4.995,0.59 -10.047,0.696 -15.063,0.317c-0.661,-0.05 -1.359,-0.122 -1.888,-0.522c-0.461,-0.348 -0.723,-0.894 -0.967,-1.418c-1.162,-2.485 -1.699,-2.754 -2.859,-5.241z" fill="#FFCD8B" id="svg_426"/>
<path d="m27.581,35.701c0,0 -7.203,0.647 -9.146,7.365c-1.943,6.718 1.781,9.308 1.781,9.308l1.376,0.081l0.648,-1.295l0.486,1.295l8.579,0.971l0.567,-0.648l0.405,0.648c0,0 4.209,-1.295 3.966,-4.694c-0.243,-3.399 -0.081,-5.018 -1.133,-7.041c-1.052,-2.023 -2.914,-4.452 -2.914,-4.452l-4.615,-1.538z" fill="#563238" id="svg_427"/>
<g id="svg_428">
<path d="m35.733,93.744l4.374,34.965l0.712,0l-2.287,-36.243c0.001,0.001 -2.799,1.917 -2.799,1.278z" fill="#2D294C" id="svg_429"/>
<path d="m45.446,92.449l16.677,36.26l0.712,0l-14.589,-37.538c0,0.001 -2.8,1.917 -2.8,1.278z" fill="#2D294C" id="svg_430"/>
<path d="m23.661,92.449l-16.677,36.26l-0.712,0l14.589,-37.538c0.001,0.001 2.8,1.917 2.8,1.278z" fill="#2D294C" id="svg_431"/>
<path d="m31.755,93.744l-4.374,34.965l-0.712,0l2.287,-36.243c-0.001,0.001 2.799,1.917 2.799,1.278z" fill="#2D294C" id="svg_432"/>
<g id="svg_433">
<rect fill="#2D294C" height="0.485" id="svg_434" transform="matrix(0.4675 -0.884 0.884 0.4675 -9.1243 344.263)" width="19.406" x="230.048508" y="-67.858469"/>
</g>
<g id="svg_435">
<rect fill="#2D294C" height="22.897" id="svg_436" transform="matrix(0.6928 -0.7211 0.7211 0.6928 -43.4672 259.313)" width="0.485" x="174.469854" y="-57.134714"/>
</g>
<g id="svg_437">
<rect fill="#2D294C" height="0.486" id="svg_438" transform="matrix(0.6874 -0.7263 0.7263 0.6874 -50.5056 245.956)" width="24.505" x="141.987717" y="-45.752078"/>
</g>
<g id="svg_439">
<polygon fill="#2D294C" id="svg_440" points="30.022003173828125,107.79200744628906 20.957000732421875,92.89999389648438 21.37298583984375,92.64700317382812 30.43798828125,107.53900146484375 "/>
</g>
<path d="m18.899,55.868c0,0 10.684,-2.59 14.245,19.101l0.56,2.427c0.816,3.538 3.897,6.093 7.525,6.241l4.676,0.191c3.044,0.124 5.701,2.099 6.697,4.978l1.262,3.645l2.586,0c0,0 -0.971,5.18 -6.151,5.18c-5.18,0 -17.155,0 -17.155,0l-11.007,0c0,0 -9.065,0.648 -10.684,-11.331c-1.619,-11.979 -7.447,-31.08 7.446,-30.432z" fill="#5A8ECC" id="svg_441"/>
<path d="m59.2,124.768l2.354,2.58l3.653,1.66c1.819,0.738 1.155,1.635 1.155,1.635l-10.017,-0.755l-0.757,-3.442l3.612,-1.678z" fill="#2D294C" id="svg_442"/>
<path d="m55.342,116.368l3.858,8.4l2.354,2.58c0,0 -1.511,0.448 -3.066,0.122c-1.555,-0.326 -2.9,-1.025 -2.9,-1.025l-4.08,-6.569l-0.47,-3.327l4.304,-0.181z" fill="#EFAFC0" id="svg_443"/>
<polygon fill="#EFAFC0" id="svg_444" points="46.364990234375,119.7550048828125 43.110992431640625,122.447998046875 43.89599609375,126.93699645996094 46.701995849609375,128.0590057373047 41.989013671875,127.94700622558594 39.519989013671875,121.55099487304688 42.217010498046875,118.19599914550781 "/>
<path d="m46.702,128.059c0,0 0.991,0.013 1.331,0.391c0.34,0.377 0,0.944 0,0.944l-7.615,-0.213l-2.581,-7.406l1.683,-0.224l2.469,6.396l4.713,0.112z" fill="#2D294C" id="svg_445"/>
<g id="svg_446">
<path d="m47.267,120.77l-7.793,-3.96l1.98,-3.896c3.337,-6.565 6.933,-14.445 8.429,-18.673c-4.337,-0.627 -12.462,-1.279 -19.433,-1.635l-4.365,-0.222l0.446,-8.73l4.365,0.222c23.576,1.202 25.569,2.862 26.639,3.754c3.192,2.661 2.048,6.952 -2.774,17.706c-2.604,5.804 -5.396,11.306 -5.513,11.538l-1.981,3.896zm5.396,-25.969l0,0l0,0z" fill="#DC614B" id="svg_447"/>
</g>
<path d="m38.418,93.753l-8.349,0.463c-4.148,0 -9.259,-3.453 -9.259,-7.601l-0.124,-2.204l16.226,-1.472l1.506,10.814z" fill="#DC614B" id="svg_448"/>
<g id="svg_449">
<path d="m49.849,123.164l-0.339,-4.357c-0.68,-8.739 -2.282,-19.42 -3.618,-21.755c-1.457,-1.115 -7.727,-2.828 -13.42,-3.641l-4.326,-0.618l1.236,-8.653l4.327,0.618c4.303,0.615 14.717,2.449 18.469,6.2c3.528,3.527 5.236,16.723 6.047,27.171l0.339,4.357l-8.715,0.678zm-4.032,-26.233l0,0l0,0z" fill="#DC614B" id="svg_450"/>
</g>
<path d="m30.622,55.47c0,0 -6.79,-0.266 -7.257,-0.224c-1.32,0.117 -3.098,0.479 -3.965,0.973c-1.958,1.114 -2.138,2.935 -2.305,4.379c-0.226,1.95 4.137,11.453 4.137,11.453l-1.692,8.038c0,0 -0.26,3.501 0,5.711l17.557,-2.327l-1.423,-6.738l-1.457,-8.822c0,0 2.514,-4.672 0.89,-6.394c-3.922,-4.16 -4.485,-6.049 -4.485,-6.049z" fill="#FFCD8B" id="svg_451"/>
<g id="svg_452">
<path d="m26.036,58.4l0,0c-1.314,0 -2.378,-1.065 -2.378,-2.379l0,-10.132c0,-0.157 0.127,-0.285 0.285,-0.285l4.188,0c0.157,0 0.285,0.127 0.285,0.285l0,10.132c-0.001,1.314 -1.066,2.379 -2.38,2.379z" fill="#EFAFC0" id="svg_453"/>
<path d="m23.658,51.307c0.63,1.633 2.003,4.149 4.757,4.768l0,-10.471l-4.3,0l-0.457,0.201l0,5.502z" fill="#D98E9D" id="svg_454" opacity="0.5"/>
</g>
<path d="m30.232,37.823l-1.413,0c-2.809,0 -5.086,2.277 -5.086,5.086l0,4.812c0,2.84 2.302,5.143 5.143,5.143l0.659,0c3.193,0 5.782,-2.589 5.782,-5.782l0,-4.173c0,-2.809 -2.277,-5.086 -5.085,-5.086z" fill="#EFAFC0" id="svg_455"/>
<path d="m25.836,46.607c0,1.511 -1.225,2.736 -2.736,2.736s-2.736,-1.225 -2.736,-2.736c0,-1.511 1.225,-2.736 2.736,-2.736s2.736,1.225 2.736,2.736z" fill="#EFAFC0" id="svg_456"/>
<path d="m23.798,42.265c0.213,0.985 0.35,1.986 0.41,2.991c0.114,-0.106 0.325,-0.155 0.47,-0.099" fill="none" id="svg_457"/>
<path d="m31.935,48.65l-2.962,-0.753c0,0 0.472,1.61 1.418,1.773c0.946,0.163 1.544,-1.02 1.544,-1.02z" fill="#FFFFFF" id="svg_458"/>
<circle cx="30.464" cy="44.299" fill="#2D294C" id="svg_459" r="0.552"/>
<path d="m34.877,44.299c0,0.305 -0.247,0.552 -0.552,0.552c-0.305,0 -0.552,-0.247 -0.552,-0.552c0,-0.305 0.247,-0.552 0.552,-0.552c0.305,0 0.552,0.247 0.552,0.552z" fill="#2D294C" id="svg_460"/>
<path d="m11.452,86.3c-0.717,-5.305 -2.256,-12.005 -2.459,-17.77l0,0l5.055,14.542c1.072,3.084 3.979,5.151 7.244,5.151l3.238,0c2.861,0 5.44,1.725 6.532,4.37l2.081,5.039l-11.007,0c0,-0.001 -9.065,0.647 -10.684,-11.332z" fill="#6F9EDE" id="svg_461"/>
<path d="m23.69,89.615c-0.835,-0.239 -1.695,-0.614 -2.165,-1.344c-0.47,-0.73 0.845,-1.456 1.633,-1.82c0.862,0.623 2.047,0.35 3.107,0.443c0.643,0.057 1.263,0.258 1.867,0.485c0.774,0.29 1.542,0.63 2.191,1.142c0.649,0.513 1.176,1.218 1.337,2.029c0.038,0.19 -0.093,0.338 -0.283,0.303c-0.19,-0.035 -0.324,-0.201 -0.449,-0.349c-0.393,-0.463 -0.874,-0.851 -1.41,-1.136c-0.097,-0.052 -0.21,0.105 -0.122,0.17c1.29,0.964 1.446,1.936 1.404,2.013c-0.046,0.083 -0.161,0.093 -0.254,0.073c-0.167,-0.037 -0.391,-0.344 -0.512,-0.464c-0.512,-0.51 -1.03,-0.69 -1.03,-0.69s-0.614,-0.087 0.722,0.964c0.164,0.129 0.329,0.293 0.351,0.5c0.004,0.037 0.003,0.075 -0.012,0.109c-0.017,0.041 -0.051,0.072 -0.088,0.096c-0.114,0.072 -0.262,0.074 -0.391,0.036c-0.129,-0.038 -0.245,-0.111 -0.359,-0.184c-0.558,-0.355 -1.116,-0.71 -1.675,-1.066c0.14,0.133 1.125,0.903 1.214,1.075c0.034,0.067 0.065,0.147 0.034,0.216c-0.044,0.102 -0.183,0.112 -0.294,0.101c-0.743,-0.075 -2.293,-1.017 -2.922,-1.421c-0.628,-0.404 -1.176,-1.076 -1.894,-1.281z" fill="#EFAFC0" id="svg_462"/>
<path d="m19.326,56.368c0,0 -7.823,5.529 -9.727,14.837c-1.904,9.307 4.138,16.479 11.694,17.018l2.888,-1.432c0,0 -9.08,-9.595 -8.445,-12.768c0.635,-3.173 5.921,-10.222 5.921,-10.222s-0.521,-8.173 -2.331,-7.433z" fill="#FFCD8B" id="svg_463"/>
</g>
<g id="svg_435">
<rect fill="#2D294C" height="22.897" id="svg_436" transform="matrix(0.6928 -0.7211 0.7211 0.6928 -43.4672 259.313)" width="0.485" x="174.469854" y="-57.134714"/>
</g>
<g id="svg_437">
<rect fill="#2D294C" height="0.486" id="svg_438" transform="matrix(0.6874 -0.7263 0.7263 0.6874 -50.5056 245.956)" width="24.505" x="141.987717" y="-45.752078"/>
</g>
<g id="svg_439">
<polygon fill="#2D294C" id="svg_440" points="30.022003173828125,107.79200744628906 20.957000732421875,92.89999389648438 21.37298583984375,92.64700317382812 30.43798828125,107.53900146484375 "/>
</g>
<path d="m18.899,55.868c0,0 10.684,-2.59 14.245,19.101l0.56,2.427c0.816,3.538 3.897,6.093 7.525,6.241l4.676,0.191c3.044,0.124 5.701,2.099 6.697,4.978l1.262,3.645l2.586,0c0,0 -0.971,5.18 -6.151,5.18c-5.18,0 -17.155,0 -17.155,0l-11.007,0c0,0 -9.065,0.648 -10.684,-11.331c-1.619,-11.979 -7.447,-31.08 7.446,-30.432z" fill="#5A8ECC" id="svg_441"/>
<path d="m59.2,124.768l2.354,2.58l3.653,1.66c1.819,0.738 1.155,1.635 1.155,1.635l-10.017,-0.755l-0.757,-3.442l3.612,-1.678z" fill="#2D294C" id="svg_442"/>
<path d="m55.342,116.368l3.858,8.4l2.354,2.58c0,0 -1.511,0.448 -3.066,0.122c-1.555,-0.326 -2.9,-1.025 -2.9,-1.025l-4.08,-6.569l-0.47,-3.327l4.304,-0.181z" fill="#EFAFC0" id="svg_443"/>
<polygon fill="#EFAFC0" id="svg_444" points="46.364990234375,119.7550048828125 43.110992431640625,122.447998046875 43.89599609375,126.93699645996094 46.701995849609375,128.0590057373047 41.989013671875,127.94700622558594 39.519989013671875,121.55099487304688 42.217010498046875,118.19599914550781 "/>
<path d="m46.702,128.059c0,0 0.991,0.013 1.331,0.391c0.34,0.377 0,0.944 0,0.944l-7.615,-0.213l-2.581,-7.406l1.683,-0.224l2.469,6.396l4.713,0.112z" fill="#2D294C" id="svg_445"/>
<g id="svg_446">
<path d="m47.267,120.77l-7.793,-3.96l1.98,-3.896c3.337,-6.565 6.933,-14.445 8.429,-18.673c-4.337,-0.627 -12.462,-1.279 -19.433,-1.635l-4.365,-0.222l0.446,-8.73l4.365,0.222c23.576,1.202 25.569,2.862 26.639,3.754c3.192,2.661 2.048,6.952 -2.774,17.706c-2.604,5.804 -5.396,11.306 -5.513,11.538l-1.981,3.896zm5.396,-25.969l0,0l0,0z" fill="#9D3542" id="svg_447"/>
</g>
<path d="m38.418,93.753l-8.349,0.463c-4.148,0 -9.259,-3.453 -9.259,-7.601l-0.124,-2.204l16.226,-1.472l1.506,10.814z" fill="#DC614B" id="svg_448"/>
<g id="svg_449">
<path d="m49.849,123.164l-0.339,-4.357c-0.68,-8.739 -2.282,-19.42 -3.618,-21.755c-1.457,-1.115 -7.727,-2.828 -13.42,-3.641l-4.326,-0.618l1.236,-8.653l4.327,0.618c4.303,0.615 14.717,2.449 18.469,6.2c3.528,3.527 5.236,16.723 6.047,27.171l0.339,4.357l-8.715,0.678zm-4.032,-26.233l0,0l0,0z" fill="#DC614B" id="svg_450"/>
</g>
<path d="m30.622,55.47c0,0 -6.79,-0.266 -7.257,-0.224c-1.32,0.117 -3.098,0.479 -3.965,0.973c-1.958,1.114 -2.138,2.935 -2.305,4.379c-0.226,1.95 4.137,11.453 4.137,11.453l-1.692,8.038c0,0 -0.26,3.501 0,5.711l17.557,-2.327l-1.423,-6.738l-1.457,-8.822c0,0 2.514,-4.672 0.89,-6.394c-3.922,-4.16 -4.485,-6.049 -4.485,-6.049z" fill="#FFCD8B" id="svg_451"/>
<g id="svg_452">
<path d="m26.036,58.4l0,0c-1.314,0 -2.378,-1.065 -2.378,-2.379l0,-10.132c0,-0.157 0.127,-0.285 0.285,-0.285l4.188,0c0.157,0 0.285,0.127 0.285,0.285l0,10.132c-0.001,1.314 -1.066,2.379 -2.38,2.379z" fill="#EFAFC0" id="svg_453"/>
<path d="m23.658,51.307c0.63,1.633 2.003,4.149 4.757,4.768l0,-10.471l-4.3,0l-0.457,0.201l0,5.502z" fill="#D98E9D" id="svg_454" opacity="0.5"/>
</g>
<path d="m30.232,37.823l-1.413,0c-2.809,0 -5.086,2.277 -5.086,5.086l0,4.812c0,2.84 2.302,5.143 5.143,5.143l0.659,0c3.193,0 5.782,-2.589 5.782,-5.782l0,-4.173c0,-2.809 -2.277,-5.086 -5.085,-5.086z" fill="#EFAFC0" id="svg_455"/>
<path d="m25.836,46.607c0,1.511 -1.225,2.736 -2.736,2.736s-2.736,-1.225 -2.736,-2.736c0,-1.511 1.225,-2.736 2.736,-2.736s2.736,1.225 2.736,2.736z" fill="#EFAFC0" id="svg_456"/>
<path d="m23.798,42.265c0.213,0.985 0.35,1.986 0.41,2.991c0.114,-0.106 0.325,-0.155 0.47,-0.099" fill="none" id="svg_457"/>
<path d="m31.935,48.65l-2.962,-0.753c0,0 0.472,1.61 1.418,1.773c0.946,0.163 1.544,-1.02 1.544,-1.02z" fill="#FFFFFF" id="svg_458"/>
<circle cx="30.464" cy="44.299" fill="#2D294C" id="svg_459" r="0.552"/>
<path d="m34.877,44.299c0,0.305 -0.247,0.552 -0.552,0.552c-0.305,0 -0.552,-0.247 -0.552,-0.552c0,-0.305 0.247,-0.552 0.552,-0.552c0.305,0 0.552,0.247 0.552,0.552z" fill="#2D294C" id="svg_460"/>
<path d="m11.452,86.3c-0.717,-5.305 -2.256,-12.005 -2.459,-17.77l0,0l5.055,14.542c1.072,3.084 3.979,5.151 7.244,5.151l3.238,0c2.861,0 5.44,1.725 6.532,4.37l2.081,5.039l-11.007,0c0,-0.001 -9.065,0.647 -10.684,-11.332z" fill="#6F9EDE" id="svg_461"/>
<path d="m23.69,89.615c-0.835,-0.239 -1.695,-0.614 -2.165,-1.344c-0.47,-0.73 0.845,-1.456 1.633,-1.82c0.862,0.623 2.047,0.35 3.107,0.443c0.643,0.057 1.263,0.258 1.867,0.485c0.774,0.29 1.542,0.63 2.191,1.142c0.649,0.513 1.176,1.218 1.337,2.029c0.038,0.19 -0.093,0.338 -0.283,0.303c-0.19,-0.035 -0.324,-0.201 -0.449,-0.349c-0.393,-0.463 -0.874,-0.851 -1.41,-1.136c-0.097,-0.052 -0.21,0.105 -0.122,0.17c1.29,0.964 1.446,1.936 1.404,2.013c-0.046,0.083 -0.161,0.093 -0.254,0.073c-0.167,-0.037 -0.391,-0.344 -0.512,-0.464c-0.512,-0.51 -1.03,-0.69 -1.03,-0.69s-0.614,-0.087 0.722,0.964c0.164,0.129 0.329,0.293 0.351,0.5c0.004,0.037 0.003,0.075 -0.012,0.109c-0.017,0.041 -0.051,0.072 -0.088,0.096c-0.114,0.072 -0.262,0.074 -0.391,0.036c-0.129,-0.038 -0.245,-0.111 -0.359,-0.184c-0.558,-0.355 -1.116,-0.71 -1.675,-1.066c0.14,0.133 1.125,0.903 1.214,1.075c0.034,0.067 0.065,0.147 0.034,0.216c-0.044,0.102 -0.183,0.112 -0.294,0.101c-0.743,-0.075 -2.293,-1.017 -2.922,-1.421c-0.628,-0.404 -1.176,-1.076 -1.894,-1.281z" fill="#EFAFC0" id="svg_462"/>
<path d="m19.326,56.368c0,0 -7.823,5.529 -9.727,14.837c-1.904,9.307 4.138,16.479 11.694,17.018l2.888,-1.432c0,0 -9.08,-9.595 -8.445,-12.768c0.635,-3.173 5.921,-10.222 5.921,-10.222s-0.521,-8.173 -2.331,-7.433z" fill="#FFCD8B" id="svg_463"/>
</g>
<path d="m24.036,44.593c0,0 7.272,-1.157 8.422,-4.988c0,0 0.668,3.521 2.86,3.303c0,0 1.247,-7.694 -6.037,-7.37c-6.637,0.295 -7.568,3.739 -7.313,8.592c0,0.001 1.62,-0.817 2.068,0.463z" fill="#563238" id="svg_464"/>
<path d="m23.442,47.902c0,0.278 -0.226,0.504 -0.504,0.504c-0.278,0 -0.504,-0.226 -0.504,-0.504c0,-0.278 0.226,-0.504 0.504,-0.504c0.278,0 0.504,0.225 0.504,0.504z" fill="#FFFFFF" id="svg_465"/>
<path d="m56.561,44.817l2.687,-2.855c0,0 -0.068,-2.583 0.408,-4.01c0.476,-1.427 0.476,-1.427 0.476,-1.427s0.748,-0.136 0.68,0.952c-0.068,1.088 0.068,2.039 0.068,2.039s3.195,-5.709 4.01,-5.981c0.816,-0.272 -0.748,3.127 -0.952,3.602c-0.204,0.476 2.651,-3.058 2.991,-2.923c0.34,0.136 -0.884,2.515 -2.039,3.806c0,0 2.787,-2.447 3.127,-2.243c0.34,0.204 -1.555,2.214 -1.555,2.214s2.708,-2.37 0.671,0.776c-0.866,1.337 -4.962,4.554 -5.437,4.758c-0.476,0.204 -2.651,3.466 -2.651,3.466l-2.484,-2.174z" fill="#EFAFC0" id="svg_466"/>
<path d="m24.036,44.593c0,0 7.272,-1.157 8.422,-4.988c0,0 0.668,3.521 2.86,3.303c0,0 1.247,-7.694 -6.037,-7.37c-6.637,0.295 -7.568,3.739 -7.313,8.592c0,0.001 1.62,-0.817 2.068,0.463z" fill="#563238" id="svg_464"/>
<path d="m23.442,47.902c0,0.278 -0.226,0.504 -0.504,0.504c-0.278,0 -0.504,-0.226 -0.504,-0.504c0,-0.278 0.226,-0.504 0.504,-0.504c0.278,0 0.504,0.225 0.504,0.504z" fill="#FFFFFF" id="svg_465"/>
<path d="m56.561,44.817l2.687,-2.855c0,0 -0.068,-2.583 0.408,-4.01c0.476,-1.427 0.476,-1.427 0.476,-1.427s0.748,-0.136 0.68,0.952c-0.068,1.088 0.068,2.039 0.068,2.039s3.195,-5.709 4.01,-5.981c0.816,-0.272 -0.748,3.127 -0.952,3.602c-0.204,0.476 2.651,-3.058 2.991,-2.923c0.34,0.136 -0.884,2.515 -2.039,3.806c0,0 2.787,-2.447 3.127,-2.243c0.34,0.204 -1.555,2.214 -1.555,2.214s2.708,-2.37 0.671,0.776c-0.866,1.337 -4.962,4.554 -5.437,4.758c-0.476,0.204 -2.651,3.466 -2.651,3.466l-2.484,-2.174z" fill="#EFAFC0" id="svg_466"/>
</g>
</g>
<g id="svg_467"/>
<g id="svg_480">
<g id="svg_481"/>
<g id="svg_500">
<g id="svg_502"/>
<g id="svg_517"/>
<g id="svg_532"/>
</g>
<g id="svg_545">
<g id="svg_553"/>
<g id="svg_555"/>
</g>
<g id="svg_570">
<g id="svg_572"/>
</g>
<g id="svg_591">
<g id="svg_592"/>
</g>
</g>
<g id="svg_596">
<g id="svg_597">
<g id="svg_600"/>
<g id="svg_612"/>
<g id="svg_621">
<g id="svg_625"/>
<g id="svg_627"/>
<g id="svg_629"/>
<g id="svg_631"/>
</g>
<g id="svg_633">
<g id="svg_637"/>
<g id="svg_639"/>
<g id="svg_641"/>
<g id="svg_643"/>
</g>
</g>
<g id="svg_645">
<g id="svg_646">
<g id="svg_647"/>
<g id="svg_650"/>
</g>
<g id="svg_656">
<g id="svg_657"/>
<g id="svg_660"/>
</g>
<g id="svg_666">
<g id="svg_671"/>
<g id="svg_677"/>
<g id="svg_680"/>
<g id="svg_691"/>
</g>
<g id="svg_700">
<g id="svg_704"/>
<g id="svg_706"/>
<g id="svg_709"/>
</g>
<g id="svg_730"/>
</g>
<g id="svg_735">
<g id="svg_736"/>
</g>
</g>
<g id="svg_740">
<g id="svg_742">
<g id="svg_743"/>
<g id="svg_746"/>
<g id="svg_749"/>
<g id="svg_752"/>
<g id="svg_755"/>
</g>
<g id="svg_758"/>
</g>
<g id="svg_761">
<g id="svg_762"/>
<g id="svg_765"/>
</g>
<g id="svg_768">
<g id="svg_771">
<g id="svg_772"/>
<g id="svg_774"/>
</g>
<g id="svg_776">
<g id="svg_777"/>
<g id="svg_779"/>
</g>
<g id="svg_781">
<g id="svg_782"/>
<g id="svg_784"/>
</g>
<g id="svg_786">
<g id="svg_787"/>
<g id="svg_789"/>
</g>
<g id="svg_791">
<g id="svg_792"/>
<g id="svg_794"/>
</g>
<g id="svg_796">
<g id="svg_797"/>
<g id="svg_799"/>
</g>
<g id="svg_801">
<g id="svg_802"/>
<g id="svg_804"/>
</g>
<g id="svg_806">
<g id="svg_807"/>
<g id="svg_809"/>
</g>
<g id="svg_811"/>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_814">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_815"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="120" height="240" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<g id="svg_107">
<g id="svg_108">
<path d="m61.3,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.115,-54.575l11.628,-9.37z" fill="#E7B598" id="svg_109"/>
<g id="svg_110">
<path d="m63.882,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.064,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.095,9.011z" fill="#3F3945" id="svg_111"/>
<path d="m58.7,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#E7B598" id="svg_112"/>
</g>
</g>
<g id="svg_113">
<path d="m47.796,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.114,-54.575l11.627,-9.37z" fill="#E7B598" id="svg_114"/>
<g id="svg_115">
<path d="m50.378,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.065,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.096,9.011z" fill="#3F3945" id="svg_116"/>
<path d="m45.196,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#E7B598" id="svg_117"/>
</g>
</g>
<path d="m37.759,96.171c0,0 -6.725,1.185 -10.812,43.664l-1.451,16.827c0,0 5.437,-1.917 14.885,2.343c9.449,4.26 30.029,-4.686 30.029,-4.686s-1.349,-39.087 -10.452,-59.906l-22.199,1.758z" fill="#C4B6A7" id="svg_118"/>
</g>
<g id="svg_119">
<path d="m36.29,33.629c0,0 0.423,6.627 -1.553,10.145c-1.976,3.518 -3.479,4.967 -4.006,8.788c0,0 16.576,4.945 33.177,-0.558l-6.522,-18.376l-21.096,0l0,0.001z" fill="#4F3639" id="svg_120"/>
<path d="m57.354,26.041c0,0 -15.531,-4.207 -19.923,3.58c-3.187,5.65 0.578,15.881 7.856,18.764c6.932,2.746 4.965,-19.842 4.965,-19.842l7.102,-2.502z" fill="#4F3639" id="svg_121"/>
<path d="m55.874,40.087c-0.157,1.865 1.227,3.504 3.092,3.661c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.865,-0.158 -3.504,1.227 -3.661,3.092z" fill="#DEA888" id="svg_122"/>
<path d="m54.352,49.556l-0.576,7.363c-0.197,2.586 -1.896,3.847 -4.44,3.633l0,0c-2.172,-0.183 -4.117,-1.476 -4.205,-3.653l1.464,-11.544l7.757,4.201z" fill="#E7B598" id="svg_123"/>
<polygon fill="#DEA888" id="svg_124" points="53.94700622558594,54.875 45.44700622558594,47.135986328125 54.425994873046875,48.803009033203125 "/>
<path d="m52.092,50.715l0,0c4.354,0.077 8,-3.3 8.182,-6.853l0.379,-8.387c0.228,-4.454 -3.118,-8.286 -7.562,-8.66l0,0c-4.549,-0.383 -9.346,2.927 -9.729,7.476l-0.231,6.749c-0.18,5.218 3.756,9.236 8.961,9.675z" fill="#E7B598" id="svg_125"/>
<path d="m48.166,30.423c1.575,3.798 -3.21,9.62 -3.21,9.62l-5.761,-6.925c-0.001,0 7.856,-5.383 8.971,-2.695z" fill="#4F3639" id="svg_126"/>
<g id="svg_127">
<path d="m39.079,40.019c-0.157,1.865 1.227,3.504 3.092,3.662c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.864,-0.159 -3.503,1.226 -3.661,3.091z" fill="#E7B598" id="svg_128"/>
<path d="m43.552,41.407c0,0 0.206,-2.448 -2.167,-2.26" fill="none" id="svg_129" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
<g id="svg_130">
<polyline fill="none" id="svg_131" points="55.81500244140625,41.614990234375 55.968994140625,43.125 54.83299255371094,43.170989990234375 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
<path d="m50.628,35.968c0,0 1.881,-0.164 1.902,0.627c0,0 0,0.535 -1.641,0.646c-1.641,0.11 -2.068,-0.013 -2.219,-0.516c-0.152,-0.503 1.076,-0.75 1.958,-0.757z" fill="#1F2326" id="svg_132"/>
<path d="m58.494,36.176c0,0 -1.553,-0.162 -1.678,0.581c0,0 -0.073,0.503 1.287,0.613c1.36,0.11 1.802,-0.069 1.929,-0.475c0.144,-0.46 -0.8,-0.709 -1.538,-0.719z" fill="#1F2326" id="svg_133"/>
<path d="m57.385,39.954c-0.013,0.45 0.33,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.436,-0.014 -0.8,0.34 -0.813,0.79z" fill="#1F2326" id="svg_134"/>
<path d="m50.465,39.732c-0.013,0.45 0.329,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.435,-0.014 -0.799,0.34 -0.813,0.79z" fill="#1F2326" id="svg_135"/>
<path d="m56.815,44.735l-5.726,0c0,0 0.905,1.788 3.263,1.779c2.148,-0.007 2.463,-1.779 2.463,-1.779z" fill="#FFFFFF" id="svg_136"/>
</g>
<path d="m47.205,30.423c0,0 3.424,5.851 14.408,3.798c0,0 2.67,-5.993 -4.259,-8.18c-6.93,-2.188 -14.898,1.508 -14.898,1.508l0,4.927l4.749,-2.053z" fill="#4F3639" id="svg_137"/>
</g>
<g id="svg_138">
<g id="svg_139">
<path d="m56.454,58.018c0,0 9.821,2.329 11.553,15.317l-6.3,5.391l-5.253,-20.708z" fill="#707DEF" id="svg_140"/>
<g id="svg_141">
<path d="m72.006,94.803c-0.177,-0.291 -0.064,2.104 -0.172,1.78c0,0 -2.925,-4.972 -7.25,-15.385c-1.106,-2.664 0.385,-10.814 3.26,-11.041c2.111,-0.167 4.618,3.77 5.121,5.827l5.243,13.186l-6.202,5.633z" fill="#E7B598" id="svg_142"/>
<path d="m73.62,90.323l20.437,-11.019l1.882,2.079l-16.512,16.377c-2.398,2.406 -6.473,1.593 -7.765,-1.548l0,0c-0.895,-2.177 -0.063,-4.681 1.958,-5.889z" fill="#E7B598" id="svg_143"/>
<path d="m101.709,78.454c1.374,1.815 0.663,4.668 -1.588,6.371c-2.251,1.703 -5.189,1.612 -6.563,-0.203c-1.374,-1.815 -0.663,-4.668 1.588,-6.371c2.251,-1.703 5.189,-1.612 6.563,0.203z" fill="#E7B598" id="svg_144"/>
</g>
</g>
<g id="svg_145">
<g id="svg_146">
<g id="svg_147">
<path d="m36.26,97.764c0.829,0.016 25.04,-0.28 25.04,-0.28l-1.802,-35.76l-17.901,3.048l-5.337,32.992z" fill="#FFFFFF" id="svg_148"/>
<path d="m41.597,57.649l7.183,-1.259l5.305,0.957l4.102,8.332c0,0 1.227,3.814 -5.311,4.419c-6.538,0.605 -11.078,-3.995 -12.167,-6.84c-1.09,-2.846 0.888,-5.609 0.888,-5.609z" fill="#E7B598" id="svg_149"/>
</g>
<g id="svg_150">
<path d="m54.085,57.347c0,0 5.376,11.324 4.043,23.017c-1.332,11.694 5.31,24.05 5.31,24.05l0,-32.265c0,0 -0.07,-13.466 -9.353,-14.802z" fill="#707DEF" id="svg_151"/>
<path d="m62.766,67.107c0,0 2.192,5.383 0.673,14.431" fill="none" id="svg_152" stroke="#4F63D8" stroke-miterlimit="10" stroke-width="0.764"/>
</g>
<path d="m41.597,57.649c0,0 16.376,20.229 -9.038,48.331l2.684,-46.65l6.354,-1.681z" fill="#707DEF" id="svg_153"/>
</g>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156">
<path d="m35.286,59.315c0,0 -12.081,2.553 -12.675,26.91l14.457,-1.365l1.98,-19.807l-3.762,-5.738z" fill="#707DEF" id="svg_157"/>
<path d="m36.679,90.803c0,0 3.15,-3.086 0.389,-5.944c0,0 -10.297,-1.601 -16.298,2.167c0,0 -2.512,4.286 0.698,6.608c0,0.001 4.326,-2.831 15.211,-2.831z" fill="#8C9BEF" id="svg_158"/>
<line fill="none" id="svg_159" stroke="#4F63D8" stroke-miterlimit="10" stroke-width="0.764" x1="37.068" x2="39.46" y1="84.86" y2="70.101"/>
</g>
<g id="svg_160">
<path d="m36.679,90.803l22.472,2.831l-1.249,4.419l-30.375,0.75c-3.043,0.075 -5.654,-2.153 -6.059,-5.169l0,0c0,0.001 6.728,-4.835 15.211,-2.831z" fill="#E7B598" id="svg_161"/>
<path d="m68.003,95.927c-0.117,2.805 -3.031,4.961 -6.508,4.816c-3.478,-0.145 -6.202,-2.536 -6.085,-5.341c0.117,-2.805 3.031,-4.961 6.508,-4.816c3.478,0.145 6.202,2.536 6.085,5.341z" fill="#E7B598" id="svg_162"/>
</g>
</g>
<g id="svg_163">
<path d="m37.285,93.313l23.99,0l0,-2.707l-23.99,0c-0.747,0 -1.353,0.606 -1.353,1.354l0,0c0,0.747 0.605,1.353 1.353,1.353z" fill="#1C2123" id="svg_164"/>
<g id="svg_165">
<path d="m58.117,93.313l34.442,0c1.831,0 3.438,-1.221 3.928,-2.984l6.199,-22.27c0.548,-1.969 -0.933,-3.918 -2.976,-3.918l-31.85,0c-1.222,0 -2.291,0.824 -2.601,2.006l-7.142,27.166z" fill="#2E353A" id="svg_166"/>
<path d="m78.798,79.973c0,1.661 1.347,3.007 3.008,3.007s3.007,-1.346 3.007,-3.007c0,-1.661 -1.346,-3.007 -3.007,-3.007s-3.008,1.346 -3.008,3.007z" fill="#FFFFFF" id="svg_167"/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

+61 -261
View File
@@ -1,94 +1,24 @@
<?xml version="1.0"?>
<svg width="120" height="240" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_4">
<g id="svg_5">
<g id="svg_6"/>
</g>
<g id="svg_13">
<g id="svg_14">
<g id="svg_15">
<g id="svg_17">
<g id="svg_18"/>
</g>
</g>
</g>
<g id="svg_22">
<g id="svg_23">
<g id="svg_25">
<g id="svg_26"/>
</g>
</g>
<g id="svg_29">
<g id="svg_31"/>
</g>
</g>
</g>
</g>
<g id="svg_33">
<g id="svg_34">
<g id="svg_35">
<g id="svg_37">
<g id="svg_38"/>
</g>
</g>
<g id="svg_42"/>
<g id="svg_45">
<g id="svg_46"/>
<g id="svg_49">
<g id="svg_50"/>
<g id="svg_53"/>
</g>
</g>
</g>
<g id="svg_57">
<g id="svg_59"/>
</g>
</g>
<g id="svg_63">
<g id="svg_69"/>
<g id="svg_72">
<g id="svg_78"/>
</g>
</g>
</g>
<g id="svg_87">
<g id="svg_89">
<g id="svg_90">
<g id="svg_91">
<g id="svg_94"/>
</g>
</g>
<g id="svg_98">
<g id="svg_99"/>
</g>
</g>
</g>
</g>
<g id="svg_105">
<g id="svg_106">
<g id="svg_107">
<?xml version="1.0" encoding="utf-8"?>
<svg width="120" height="240" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<g id="svg_107">
<g id="svg_108">
<path d="m61.3,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.115,-54.575l11.628,-9.37z" fill="#EDBC9C" id="svg_109"/>
<g id="svg_110">
<path d="m63.882,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.064,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.095,9.011z" fill="#20202A" id="svg_111"/>
<path d="m58.7,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#EDBC9C" id="svg_112"/>
</g>
<path d="m61.3,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.115,-54.575l11.628,-9.37z" fill="#EDBC9C" id="svg_109"/>
<g id="svg_110">
<path d="m63.882,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.064,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.095,9.011z" fill="#20202A" id="svg_111"/>
<path d="m58.7,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#EDBC9C" id="svg_112"/>
</g>
</g>
<g id="svg_113">
<path d="m47.796,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.114,-54.575l11.627,-9.37z" fill="#EDBC9C" id="svg_114"/>
<g id="svg_115">
<path d="m50.378,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.065,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.096,9.011z" fill="#20202A" id="svg_116"/>
<path d="m45.196,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#EDBC9C" id="svg_117"/>
</g>
<path d="m47.796,141.244l-2.689,64.307l-4.824,-0.362c0,0 -5.422,-36.369 -4.114,-54.575l11.627,-9.37z" fill="#EDBC9C" id="svg_114"/>
<g id="svg_115">
<path d="m50.378,214.201c0,0 6.033,2.309 6.64,4.92l-13.121,0l-2.065,-4.069l-1.397,4.069l-1.412,0c0,0 -1.324,-10.555 1.259,-13.931l10.096,9.011z" fill="#20202A" id="svg_116"/>
<path d="m45.196,203.276l-0.089,2.275l5.837,9.767c0,0 -6.44,1.208 -8.852,-3.375c-2.412,-4.583 -1.809,-6.754 -1.809,-6.754l-0.33,-2.194l5.243,0.281z" fill="#EDBC9C" id="svg_117"/>
</g>
</g>
<path d="m37.759,96.171c0,0 -6.725,1.185 -10.812,43.664l-1.451,16.827c0,0 5.437,-1.917 14.885,2.343c9.449,4.26 30.029,-4.686 30.029,-4.686s-1.349,-39.087 -10.452,-59.906l-22.199,1.758z" fill="#C4B6A7" id="svg_118"/>
</g>
<g id="svg_119">
</g>
<g id="svg_119">
<path d="m36.29,33.629c0,0 0.423,6.627 -1.553,10.145c-1.976,3.518 -3.479,4.967 -4.006,8.788c0,0 16.576,4.945 33.177,-0.558l-6.522,-18.376l-21.096,0l0,0.001z" fill="#412424" id="svg_120"/>
<path d="m57.354,26.041c0,0 -15.531,-4.207 -19.923,3.58c-3.187,5.65 0.578,15.881 7.856,18.764c6.932,2.746 4.965,-19.842 4.965,-19.842l7.102,-2.502z" fill="#412424" id="svg_121"/>
<path d="m55.874,40.087c-0.157,1.865 1.227,3.504 3.092,3.661c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.865,-0.158 -3.504,1.227 -3.661,3.092z" fill="#DEA888" id="svg_122"/>
@@ -97,191 +27,61 @@
<path d="m52.092,50.715l0,0c4.354,0.077 8,-3.3 8.182,-6.853l0.379,-8.387c0.228,-4.454 -3.118,-8.286 -7.562,-8.66l0,0c-4.549,-0.383 -9.346,2.927 -9.729,7.476l-0.231,6.749c-0.18,5.218 3.756,9.236 8.961,9.675z" fill="#EDBC9C" id="svg_125"/>
<path d="m48.166,30.423c1.575,3.798 -3.21,9.62 -3.21,9.62l-5.761,-6.925c-0.001,0 7.856,-5.383 8.971,-2.695z" fill="#412424" id="svg_126"/>
<g id="svg_127">
<path d="m39.079,40.019c-0.157,1.865 1.227,3.504 3.092,3.662c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.864,-0.159 -3.503,1.226 -3.661,3.091z" fill="#EDBC9C" id="svg_128"/>
<path d="m43.552,41.407c0,0 0.206,-2.448 -2.167,-2.26" fill="none" id="svg_129" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
<path d="m39.079,40.019c-0.157,1.865 1.227,3.504 3.092,3.662c1.865,0.157 3.504,-1.227 3.661,-3.092c0.157,-1.865 -1.227,-3.504 -3.092,-3.661c-1.864,-0.159 -3.503,1.226 -3.661,3.091z" fill="#EDBC9C" id="svg_128"/>
<path d="m43.552,41.407c0,0 0.206,-2.448 -2.167,-2.26" fill="none" id="svg_129" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
<g id="svg_130">
<polyline fill="none" id="svg_131" points="55.81500244140625,41.614990234375 55.968994140625,43.125 54.83299255371094,43.170989990234375 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
<path d="m50.628,35.968c0,0 1.881,-0.164 1.902,0.627c0,0 0,0.535 -1.641,0.646c-1.641,0.11 -2.068,-0.013 -2.219,-0.516c-0.152,-0.503 1.076,-0.75 1.958,-0.757z" fill="#1F2326" id="svg_132"/>
<path d="m58.494,36.176c0,0 -1.553,-0.162 -1.678,0.581c0,0 -0.073,0.503 1.287,0.613c1.36,0.11 1.802,-0.069 1.929,-0.475c0.144,-0.46 -0.8,-0.709 -1.538,-0.719z" fill="#1F2326" id="svg_133"/>
<path d="m57.385,39.954c-0.013,0.45 0.33,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.436,-0.014 -0.8,0.34 -0.813,0.79z" fill="#1F2326" id="svg_134"/>
<path d="m50.465,39.732c-0.013,0.45 0.329,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.435,-0.014 -0.799,0.34 -0.813,0.79z" fill="#1F2326" id="svg_135"/>
<path d="m56.815,44.735l-5.726,0c0,0 0.905,1.788 3.263,1.779c2.148,-0.007 2.463,-1.779 2.463,-1.779z" fill="#FFFFFF" id="svg_136"/>
<polyline fill="none" id="svg_131" points="55.81500244140625,41.614990234375 55.968994140625,43.125 54.83299255371094,43.170989990234375 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
<path d="m50.628,35.968c0,0 1.881,-0.164 1.902,0.627c0,0 0,0.535 -1.641,0.646c-1.641,0.11 -2.068,-0.013 -2.219,-0.516c-0.152,-0.503 1.076,-0.75 1.958,-0.757z" fill="#1F2326" id="svg_132"/>
<path d="m58.494,36.176c0,0 -1.553,-0.162 -1.678,0.581c0,0 -0.073,0.503 1.287,0.613c1.36,0.11 1.802,-0.069 1.929,-0.475c0.144,-0.46 -0.8,-0.709 -1.538,-0.719z" fill="#1F2326" id="svg_133"/>
<path d="m57.385,39.954c-0.013,0.45 0.33,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.436,-0.014 -0.8,0.34 -0.813,0.79z" fill="#1F2326" id="svg_134"/>
<path d="m50.465,39.732c-0.013,0.45 0.329,0.826 0.766,0.84c0.436,0.014 0.8,-0.34 0.813,-0.79c0.013,-0.45 -0.329,-0.826 -0.766,-0.84c-0.435,-0.014 -0.799,0.34 -0.813,0.79z" fill="#1F2326" id="svg_135"/>
<path d="m56.815,44.735l-5.726,0c0,0 0.905,1.788 3.263,1.779c2.148,-0.007 2.463,-1.779 2.463,-1.779z" fill="#FFFFFF" id="svg_136"/>
</g>
<path d="m47.205,30.423c0,0 3.424,5.851 14.408,3.798c0,0 2.67,-5.993 -4.259,-8.18c-6.93,-2.188 -14.898,1.508 -14.898,1.508l0,4.927l4.749,-2.053z" fill="#412424" id="svg_137"/>
</g>
<g id="svg_138">
</g>
<g id="svg_138">
<g id="svg_139">
<path d="m56.454,58.018c0,0 9.821,2.329 11.553,15.317l-6.3,5.391l-5.253,-20.708z" fill="#808DFF" id="svg_140"/>
<g id="svg_141">
<path d="m72.006,94.803c-0.177,-0.291 -0.064,2.104 -0.172,1.78c0,0 -2.925,-4.972 -7.25,-15.385c-1.106,-2.664 0.385,-10.814 3.26,-11.041c2.111,-0.167 4.618,3.77 5.121,5.827l5.243,13.186l-6.202,5.633z" fill="#EDBC9C" id="svg_142"/>
<path d="m73.62,90.323l20.437,-11.019l1.882,2.079l-16.512,16.377c-2.398,2.406 -6.473,1.593 -7.765,-1.548l0,0c-0.895,-2.177 -0.063,-4.681 1.958,-5.889z" fill="#EDBC9C" id="svg_143"/>
<path d="m101.709,78.454c1.374,1.815 0.663,4.668 -1.588,6.371c-2.251,1.703 -5.189,1.612 -6.563,-0.203c-1.374,-1.815 -0.663,-4.668 1.588,-6.371c2.251,-1.703 5.189,-1.612 6.563,0.203z" fill="#EDBC9C" id="svg_144"/>
</g>
<path d="m56.454,58.018c0,0 9.821,2.329 11.553,15.317l-6.3,5.391l-5.253,-20.708z" fill="#808DFF" id="svg_140"/>
<g id="svg_141">
<path d="m72.006,94.803c-0.177,-0.291 -0.064,2.104 -0.172,1.78c0,0 -2.925,-4.972 -7.25,-15.385c-1.106,-2.664 0.385,-10.814 3.26,-11.041c2.111,-0.167 4.618,3.77 5.121,5.827l5.243,13.186l-6.202,5.633z" fill="#EDBC9C" id="svg_142"/>
<path d="m73.62,90.323l20.437,-11.019l1.882,2.079l-16.512,16.377c-2.398,2.406 -6.473,1.593 -7.765,-1.548l0,0c-0.895,-2.177 -0.063,-4.681 1.958,-5.889z" fill="#EDBC9C" id="svg_143"/>
<path d="m101.709,78.454c1.374,1.815 0.663,4.668 -1.588,6.371c-2.251,1.703 -5.189,1.612 -6.563,-0.203c-1.374,-1.815 -0.663,-4.668 1.588,-6.371c2.251,-1.703 5.189,-1.612 6.563,0.203z" fill="#EDBC9C" id="svg_144"/>
</g>
</g>
<g id="svg_145">
<g id="svg_146">
<g id="svg_147">
<path d="m36.26,97.764c0.829,0.016 25.04,-0.28 25.04,-0.28l-1.802,-35.76l-17.901,3.048l-5.337,32.992z" fill="#FFFFFF" id="svg_148"/>
<path d="m41.597,57.649l7.183,-1.259l5.305,0.957l4.102,8.332c0,0 1.227,3.814 -5.311,4.419c-6.538,0.605 -11.078,-3.995 -12.167,-6.84c-1.09,-2.846 0.888,-5.609 0.888,-5.609z" fill="#EDBC9C" id="svg_149"/>
<g id="svg_146">
<g id="svg_147">
<path d="m36.26,97.764c0.829,0.016 25.04,-0.28 25.04,-0.28l-1.802,-35.76l-17.901,3.048l-5.337,32.992z" fill="#FFFFFF" id="svg_148"/>
<path d="m41.597,57.649l7.183,-1.259l5.305,0.957l4.102,8.332c0,0 1.227,3.814 -5.311,4.419c-6.538,0.605 -11.078,-3.995 -12.167,-6.84c-1.09,-2.846 0.888,-5.609 0.888,-5.609z" fill="#EDBC9C" id="svg_149"/>
</g>
<g id="svg_150">
<path d="m54.085,57.347c0,0 5.376,11.324 4.043,23.017c-1.332,11.694 5.31,24.05 5.31,24.05l0,-32.265c0,0 -0.07,-13.466 -9.353,-14.802z" fill="#808DFF" id="svg_151"/>
<path d="m62.766,67.107c0,0 2.192,5.383 0.673,14.431" fill="none" id="svg_152" stroke="#5F73E8" stroke-miterlimit="10" stroke-width="0.764"/>
</g>
<path d="m41.597,57.649c0,0 16.376,20.229 -9.038,48.331l2.684,-46.65l6.354,-1.681z" fill="#808DFF" id="svg_153"/>
</g>
<g id="svg_150">
<path d="m54.085,57.347c0,0 5.376,11.324 4.043,23.017c-1.332,11.694 5.31,24.05 5.31,24.05l0,-32.265c0,0 -0.07,-13.466 -9.353,-14.802z" fill="#808DFF" id="svg_151"/>
<path d="m62.766,67.107c0,0 2.192,5.383 0.673,14.431" fill="none" id="svg_152" stroke="#5F73E8" stroke-miterlimit="10" stroke-width="0.764"/>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156">
<path d="m35.286,59.315c0,0 -12.081,2.553 -12.675,26.91l14.457,-1.365l1.98,-19.807l-3.762,-5.738z" fill="#808DFF" id="svg_157"/>
<path d="m36.679,90.803c0,0 3.15,-3.086 0.389,-5.944c0,0 -10.297,-1.601 -16.298,2.167c0,0 -2.512,4.286 0.698,6.608c0,0.001 4.326,-2.831 15.211,-2.831z" fill="#9CABFF" id="svg_158"/>
<line fill="none" id="svg_159" stroke="#5F73E8" stroke-miterlimit="10" stroke-width="0.764" x1="37.068" x2="39.46" y1="84.86" y2="70.101"/>
</g>
<g id="svg_160">
<path d="m36.679,90.803l22.472,2.831l-1.249,4.419l-30.375,0.75c-3.043,0.075 -5.654,-2.153 -6.059,-5.169l0,0c0,0.001 6.728,-4.835 15.211,-2.831z" fill="#EDBC9C" id="svg_161"/>
<path d="m68.003,95.927c-0.117,2.805 -3.031,4.961 -6.508,4.816c-3.478,-0.145 -6.202,-2.536 -6.085,-5.341c0.117,-2.805 3.031,-4.961 6.508,-4.816c3.478,0.145 6.202,2.536 6.085,5.341z" fill="#EDBC9C" id="svg_162"/>
</g>
</g>
<g id="svg_163">
<path d="m37.285,93.313l23.99,0l0,-2.707l-23.99,0c-0.747,0 -1.353,0.606 -1.353,1.354l0,0c0,0.747 0.605,1.353 1.353,1.353z" fill="#1C2123" id="svg_164"/>
<g id="svg_165">
<path d="m58.117,93.313l34.442,0c1.831,0 3.438,-1.221 3.928,-2.984l6.199,-22.27c0.548,-1.969 -0.933,-3.918 -2.976,-3.918l-31.85,0c-1.222,0 -2.291,0.824 -2.601,2.006l-7.142,27.166z" fill="#2E353A" id="svg_166"/>
<path d="m78.798,79.973c0,1.661 1.347,3.007 3.008,3.007s3.007,-1.346 3.007,-3.007c0,-1.661 -1.346,-3.007 -3.007,-3.007s-3.008,1.346 -3.008,3.007z" fill="#FFFFFF" id="svg_167"/>
</g>
</g>
</g>
<path d="m41.597,57.649c0,0 16.376,20.229 -9.038,48.331l2.684,-46.65l6.354,-1.681z" fill="#808DFF" id="svg_153"/>
</g>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156">
<path d="m35.286,59.315c0,0 -12.081,2.553 -12.675,26.91l14.457,-1.365l1.98,-19.807l-3.762,-5.738z" fill="#808DFF" id="svg_157"/>
<path d="m36.679,90.803c0,0 3.15,-3.086 0.389,-5.944c0,0 -10.297,-1.601 -16.298,2.167c0,0 -2.512,4.286 0.698,6.608c0,0.001 4.326,-2.831 15.211,-2.831z" fill="#9CABFF" id="svg_158"/>
<line fill="none" id="svg_159" stroke="#5F73E8" stroke-miterlimit="10" stroke-width="0.764" x1="37.068" x2="39.46" y1="84.86" y2="70.101"/>
</g>
<g id="svg_160">
<path d="m36.679,90.803l22.472,2.831l-1.249,4.419l-30.375,0.75c-3.043,0.075 -5.654,-2.153 -6.059,-5.169l0,0c0,0.001 6.728,-4.835 15.211,-2.831z" fill="#EDBC9C" id="svg_161"/>
<path d="m68.003,95.927c-0.117,2.805 -3.031,4.961 -6.508,4.816c-3.478,-0.145 -6.202,-2.536 -6.085,-5.341c0.117,-2.805 3.031,-4.961 6.508,-4.816c3.478,0.145 6.202,2.536 6.085,5.341z" fill="#EDBC9C" id="svg_162"/>
</g>
</g>
<g id="svg_163">
<path d="m37.285,93.313l23.99,0l0,-2.707l-23.99,0c-0.747,0 -1.353,0.606 -1.353,1.354l0,0c0,0.747 0.605,1.353 1.353,1.353z" fill="#1C2123" id="svg_164"/>
<g id="svg_165">
<path d="m58.117,93.313l34.442,0c1.831,0 3.438,-1.221 3.928,-2.984l6.199,-22.27c0.548,-1.969 -0.933,-3.918 -2.976,-3.918l-31.85,0c-1.222,0 -2.291,0.824 -2.601,2.006l-7.142,27.166z" fill="#2E353A" id="svg_166"/>
<path d="m78.798,79.973c0,1.661 1.347,3.007 3.008,3.007s3.007,-1.346 3.007,-3.007c0,-1.661 -1.346,-3.007 -3.007,-3.007s-3.008,1.346 -3.008,3.007z" fill="#FFFFFF" id="svg_167"/>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="svg_168">
<g id="svg_170">
<g id="svg_171">
<g id="svg_172"/>
<g id="svg_174"/>
</g>
<g id="svg_178">
<g id="svg_179">
<g id="svg_180"/>
</g>
<g id="svg_183"/>
</g>
</g>
</g>
</g>
<g id="svg_186">
<g id="svg_187">
<g id="svg_188">
<g id="svg_189">
<g id="svg_191">
<g id="svg_192"/>
<g id="svg_195"/>
</g>
</g>
<g id="svg_199">
<g id="svg_201">
<g id="svg_202"/>
<g id="svg_205"/>
</g>
</g>
<g id="svg_209"/>
</g>
<g id="svg_212">
<g id="svg_213"/>
<g id="svg_216"/>
<g id="svg_219">
<g id="svg_220">
<g id="svg_222"/>
</g>
<g id="svg_225">
<g id="svg_226"/>
<g id="svg_229"/>
</g>
</g>
</g>
<g id="svg_232">
<g id="svg_233"/>
</g>
<g id="svg_239">
<g id="svg_246"/>
<g id="svg_254">
<g id="svg_255"/>
</g>
</g>
</g>
<g id="svg_259">
<g id="svg_261">
<g id="svg_262">
<g id="svg_264"/>
</g>
</g>
</g>
</g>
<g id="svg_268">
<g id="svg_269">
<g id="svg_270">
<g id="svg_271">
<g id="svg_275"/>
</g>
<g id="svg_281">
<g id="svg_282">
<g id="svg_283">
<g id="svg_284">
<g id="svg_286"/>
</g>
<g id="svg_292"/>
</g>
<g id="svg_295">
<g id="svg_296">
<g id="svg_298"/>
</g>
<g id="svg_304"/>
</g>
</g>
<g id="svg_307">
<g id="svg_308">
<g id="svg_309">
<g id="svg_310"/>
</g>
</g>
<g id="svg_315"/>
<g id="svg_318">
<g id="svg_319"/>
<g id="svg_322">
<g id="svg_323"/>
<g id="svg_326"/>
</g>
</g>
</g>
<g id="svg_329">
<g id="svg_336"/>
<g id="svg_343"/>
</g>
</g>
</g>
<g id="svg_350">
<g id="svg_351">
<g id="svg_352"/>
<g id="svg_355"/>
<g id="svg_358">
<g id="svg_360"/>
</g>
</g>
<g id="svg_363"/>
</g>
</g>
<g id="svg_368">
<g id="svg_370"/>
</g>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_373">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_374"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

+101
View File
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="240" height="240" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<g id="svg_188">
<g id="svg_189">
<polygon fill="#E7B598" id="svg_190" points="105.89300537109375,190.30499267578125 107.11898803710938,208.34300231933594 115.42999267578125,207.98500061035156 115.1409912109375,189.718994140625 "/>
<g id="svg_191">
<g id="svg_192">
<path d="m106.501,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_193"/>
<rect fill="#CCCCCC" height="2.874" id="svg_194" width="27.532" x="89.751" y="215.94"/>
</g>
<g id="svg_195">
<line fill="none" id="svg_196" stroke="#CCCCCC" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="101.835" x2="103.478" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_197" stroke="#CCCCCC" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="99.631" x2="101.286" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_198" stroke="#CCCCCC" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="97.419" x2="99.168" y1="210.239" y2="211.73"/>
</g>
</g>
</g>
<g id="svg_199">
<polygon fill="#E7B598" id="svg_200" points="132.95599365234375,190.30499267578125 134.1820068359375,208.34300231933594 142.49301147460938,207.98500061035156 142.20401000976562,189.718994140625 "/>
<g id="svg_201">
<g id="svg_202">
<path d="m133.563,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_203"/>
<rect fill="#CCCCCC" height="2.874" id="svg_204" width="27.532" x="116.814" y="215.94"/>
</g>
<g id="svg_205">
<line fill="none" id="svg_206" stroke="#CCCCCC" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="128.898" x2="130.54" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_207" stroke="#CCCCCC" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="126.694" x2="128.349" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_208" stroke="#CCCCCC" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="124.482" x2="126.231" y1="210.239" y2="211.73"/>
</g>
</g>
</g>
<g id="svg_209">
<polygon fill="#A69382" id="svg_210" points="110.69400024414062,105.5979995727539 142.58099365234375,108.02799987792969 145.22799682617188,197.60800170898438 129.34298706054688,197.60800170898438 123.68499755859375,130.04200744628906 116.10400390625,158.91400146484375 118.68499755859375,198.6999969482422 103.80899047851562,198.7429962158203 98.67599487304688,154.75100708007812 "/>
<line fill="none" id="svg_211" stroke="#A69382" stroke-miterlimit="10" stroke-width="0.75" x1="116.554" x2="123.685" y1="126.149" y2="130.042"/>
</g>
</g>
<g id="svg_212">
<g id="svg_213">
<polygon fill="#EBAB9D" id="svg_214" points="111.10598754882812,78.26899719238281 111.31298828125,87.06099700927734 106.32199096679688,87.125 106.69000244140625,77.66100311279297 "/>
<path d="m110.799,63.347c0,0 -6.674,4.388 -5.975,15.518l6.489,0.73l-0.514,-16.248z" fill="#707DEF" id="svg_215"/>
</g>
<g id="svg_216">
<path d="m126.881,58.394c8.026,0 17.123,2.774 17.123,2.774l-0.346,51.05l-33.9,-2.604l0.359,-44.65c0.007,-0.79 0.442,-1.514 1.136,-1.891l9.375,-4.011c0,0.001 4.615,-0.668 6.253,-0.668z" fill="#707DEF" id="svg_217"/>
<line fill="none" id="svg_218" stroke="#566AE1" stroke-miterlimit="10" stroke-width="0.764" x1="109.995" x2="109.995" y1="79.446" y2="68.69"/>
</g>
<g id="svg_219">
<g id="svg_220">
<path d="m139.987,78.372l2.181,25.251c0,0 2.408,5.078 6.667,-0.204c4.259,-5.282 0.614,-25.916 0.614,-25.916l-9.462,0.869z" fill="#E7B598" id="svg_221"/>
<g id="svg_222">
<path d="m137.918,80.866c0,0 -1.819,-18.64 4.081,-19.735c6.117,-1.136 9.67,10.396 10.236,18.659l-14.317,1.076z" fill="#707DEF" id="svg_223"/>
<path d="m140.187,80.695l-2.269,0.171c0,0 -0.664,-6.379 -0.265,-10.813" fill="none" id="svg_224" stroke="#566AE1" stroke-miterlimit="10" stroke-width="0.764"/>
</g>
</g>
<g id="svg_225">
<g id="svg_226">
<path d="m125.509,79.979c0,0 -1.228,-4.452 -5.684,-4.886c-4.456,-0.434 -5.767,3.064 -4.928,5.368c0.839,2.304 3.738,6.054 8.13,4.136c4.392,-1.918 2.482,-4.618 2.482,-4.618z" fill="#E7B598" id="svg_227"/>
<path d="m118.346,75.099c0,0 -4.376,0.502 -7.633,2.089c-0.331,0.161 -0.512,0.525 -0.464,0.89l0,0c0.065,0.493 0.531,0.831 1.02,0.74l6.039,-1.129l1.038,-2.59z" fill="#E7B598" id="svg_228"/>
</g>
<g id="svg_229">
<polygon fill="#E7B598" id="svg_230" points="143.11099243164062,104.90899658203125 123.0260009765625,84.59700012207031 125.50900268554688,79.97899627685547 145.04400634765625,92.39099884033203 "/>
<line fill="none" id="svg_231" stroke="#DEA888" stroke-miterlimit="10" stroke-width="0.764" x1="140.974" x2="145.044" y1="89.805" y2="92.391"/>
</g>
</g>
</g>
</g>
<g id="svg_232">
<g id="svg_233">
<path d="m98.081,85.09l8.447,25.074c0.29,0.862 1.151,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.042c-0.296,-0.853 -1.151,-1.381 -2.047,-1.264l-20.282,2.664c-1.19,0.157 -1.94,1.361 -1.557,2.498z" fill="#222324" id="svg_234"/>
<polygon fill="#222324" id="svg_235" points="98.44100952148438,83.23100280761719 97.36801147460938,84.6240005493164 129.35501098632812,109.32099914550781 130.50201416015625,107.82599639892578 "/>
<path d="m97.114,86.335l8.447,25.074c0.29,0.862 1.15,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.04c-0.296,-0.853 -1.151,-1.381 -2.047,-1.263l-20.282,2.664c-1.19,0.155 -1.94,1.359 -1.557,2.495z" fill="#556167" id="svg_236"/>
<path d="m109.496,94.445c0.009,1.221 1.005,2.204 2.226,2.195c1.221,-0.009 2.204,-1.005 2.195,-2.226c-0.009,-1.221 -1.005,-2.204 -2.226,-2.195c-1.221,0.008 -2.204,1.005 -2.195,2.226z" fill="#86969C" id="svg_237"/>
</g>
<path d="m98.071,93.622l1.923,5.188c0.235,0.633 0.876,1.019 1.545,0.931l0,0c1.97,-0.259 3.232,-2.23 2.642,-4.128l-0.541,-1.74c-0.433,-1.394 -1.754,-2.319 -3.212,-2.25l-1.07,0.051c-0.977,0.047 -1.627,1.03 -1.287,1.948z" fill="#E7B598" id="svg_238"/>
</g>
<g id="svg_239">
<path d="m119.195,26.137c0,0 19.114,-5.53 12.146,14.151l-3.095,-6.884l-9.051,-7.267z" fill="#4F3639" id="svg_240"/>
<path d="m111.261,33.648c0,0 -4.703,2.124 0.548,8.693l1.677,-9.011l-2.225,0.318z" fill="#4F3639" id="svg_241"/>
<path d="m116.028,43.543c0.332,1.727 -0.755,3.394 -2.427,3.723c-1.672,0.329 -3.296,-0.805 -3.628,-2.532c-0.332,-1.727 0.755,-3.394 2.426,-3.723c1.673,-0.33 3.298,0.804 3.629,2.532z" fill="#DEA888" id="svg_242"/>
<path d="m119.478,50.804l1.02,8.228c0.352,2.89 2.265,4.218 5.017,3.848l0,0c2.349,-0.315 4.79,-1.776 4.371,-4.31l-1.213,-12.919l-9.195,5.153z" fill="#E7B598" id="svg_243"/>
<path d="m120.34,57.913c0,0 9.269,-3.364 8.49,-11.668c-0.232,-2.474 -9.473,3.717 -9.473,3.717l0.983,7.951z" fill="#DEA888" id="svg_244"/>
<path d="m122.243,53.961l0,0c-5.105,0.903 -8.527,-3.188 -8.757,-5.275l-2.19,-9.681c-0.788,-4.945 2.364,-9.612 7.123,-10.548l0,0c4.872,-0.958 10.458,2.177 11.425,7.21l0.911,6.449c0.828,5.806 -2.939,10.749 -8.512,11.845z" fill="#E7B598" id="svg_245"/>
<g id="svg_246">
<polyline fill="none" id="svg_247" points="117.26400756835938,45.770999908447266 117.29000854492188,47.65800094604492 118.43499755859375,47.56100082397461 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5281"/>
<path d="m121.502,40.326c0,0 -1.907,0.072 -1.828,0.868c0,0 0.068,0.537 1.727,0.441c1.659,-0.096 2.072,-0.273 2.16,-0.796c0.089,-0.524 -1.173,-0.617 -2.059,-0.513z" fill="#1F2326" id="svg_248"/>
<path d="m113.909,41.295c0,0 1.537,-0.358 1.756,0.372c0,0 0.136,0.495 -1.214,0.777c-1.35,0.282 -1.816,0.157 -1.995,-0.234c-0.201,-0.444 0.715,-0.812 1.453,-0.915z" fill="#1F2326" id="svg_249"/>
<path d="m115.497,44.944c0.07,0.45 -0.227,0.87 -0.662,0.939c-0.436,0.069 -0.845,-0.24 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.435,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_250"/>
<path d="m122.407,43.851c0.07,0.45 -0.226,0.87 -0.662,0.939c-0.435,0.068 -0.845,-0.241 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.436,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_251"/>
<path d="m117.256,49.092l4.875,-0.54c0,0 -0.146,1.713 -2.692,1.714c-1.653,0.001 -2.183,-1.174 -2.183,-1.174z" fill="#FFFFFF" id="svg_252"/>
</g>
<path d="m131.754,33.022l-0.413,7.266l-1.408,1.572l0,0c-1.085,-0.37 -2.49,-0.348 -3.389,-1.572c-0.665,-0.904 -1.326,-2.593 -1.349,-3.715l-0.032,-1.564l-3.829,2.827c-0.6,-1.4 -1.279,-3.264 -1.279,-3.264s-0.913,0.065 -2.157,3.101c0,0 -9.875,2.985 -9.87,-3.036c0.005,-6.355 11.168,-8.498 11.168,-8.498l9.609,3.278l2.949,3.605z" fill="#4F3639" id="svg_253"/>
<g id="svg_254">
<g id="svg_255">
<path d="m135.054,42.129c0.358,1.864 -0.814,3.662 -2.618,4.017c-1.804,0.355 -3.557,-0.868 -3.915,-2.732c-0.358,-1.864 0.814,-3.662 2.618,-4.017c1.805,-0.356 3.557,0.868 3.915,2.732z" fill="#E7B598" id="svg_256"/>
<path d="m130.839,43.999c0,0 -0.47,-2.447 1.868,-2.507" fill="none" id="svg_257" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
</g>
<path d="m118.435,60.001l4,-1.86l7.054,0.202l4.137,0.578c0,0 1.542,6.197 -8.92,6.542c-8.365,0.276 -6.271,-5.462 -6.271,-5.462z" fill="#E7B598" id="svg_258"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

+67 -263
View File
@@ -1,200 +1,79 @@
<?xml version="1.0"?>
<svg width="240" height="240" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<g class="layer">
<title>Layer 1</title>
<g id="BACKGROUND"/>
<g id="OBJECTS">
<g id="svg_2">
<g id="svg_3">
<g id="svg_4">
<g id="svg_5">
<g id="svg_6"/>
</g>
<g id="svg_13">
<g id="svg_14">
<g id="svg_15">
<g id="svg_17">
<g id="svg_18"/>
</g>
</g>
</g>
<g id="svg_22">
<g id="svg_23">
<g id="svg_25">
<g id="svg_26"/>
</g>
</g>
<g id="svg_29">
<g id="svg_31"/>
</g>
</g>
</g>
</g>
<g id="svg_33">
<g id="svg_34">
<g id="svg_35">
<g id="svg_37">
<g id="svg_38"/>
</g>
</g>
<g id="svg_42"/>
<g id="svg_45">
<g id="svg_46"/>
<g id="svg_49">
<g id="svg_50"/>
<g id="svg_53"/>
</g>
</g>
</g>
<g id="svg_57">
<g id="svg_59"/>
</g>
</g>
<g id="svg_63">
<g id="svg_69"/>
<g id="svg_72">
<g id="svg_78"/>
</g>
</g>
</g>
<g id="svg_87">
<g id="svg_89">
<g id="svg_90">
<g id="svg_91">
<g id="svg_94"/>
</g>
</g>
<g id="svg_98">
<g id="svg_99"/>
</g>
</g>
</g>
</g>
<g id="svg_105">
<g id="svg_106">
<g id="svg_107">
<g id="svg_108">
<g id="svg_110"/>
</g>
<g id="svg_113">
<g id="svg_115"/>
</g>
</g>
<g id="svg_119">
<g id="svg_127"/>
<g id="svg_130"/>
</g>
<g id="svg_138">
<g id="svg_139">
<g id="svg_141"/>
</g>
<g id="svg_145">
<g id="svg_146">
<g id="svg_147"/>
<g id="svg_150"/>
</g>
<g id="svg_154">
<g id="svg_155">
<g id="svg_156"/>
<g id="svg_160"/>
</g>
<g id="svg_163">
<g id="svg_165"/>
</g>
</g>
</g>
</g>
</g>
<g id="svg_168">
<g id="svg_170">
<g id="svg_171">
<g id="svg_172"/>
<g id="svg_174"/>
</g>
<g id="svg_178">
<g id="svg_179">
<g id="svg_180"/>
</g>
<g id="svg_183"/>
</g>
</g>
</g>
</g>
<g id="svg_186">
<g id="svg_187">
<g id="svg_188">
<?xml version="1.0" encoding="utf-8"?>
<svg width="240" height="240" xmlns="http://www.w3.org/2000/svg">
<g class="layer">
<g id="svg_188">
<g id="svg_189">
<polygon fill="#EDBC9C" id="svg_190" points="105.89300537109375,190.30499267578125 107.11898803710938,208.34300231933594 115.42999267578125,207.98500061035156 115.1409912109375,189.718994140625 "/>
<g id="svg_191">
<g id="svg_192">
<path d="m106.501,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_193"/>
<rect fill="#FFFFFF" height="2.874" id="svg_194" width="27.532" x="89.751" y="215.94"/>
<polygon fill="#EDBC9C" id="svg_190" points="105.89300537109375,190.30499267578125 107.11898803710938,208.34300231933594 115.42999267578125,207.98500061035156 115.1409912109375,189.718994140625 "/>
<g id="svg_191">
<g id="svg_192">
<path d="m106.501,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_193"/>
<rect fill="#FFFFFF" height="2.874" id="svg_194" width="27.532" x="89.751" y="215.94"/>
</g>
<g id="svg_195">
<line fill="none" id="svg_196" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="101.835" x2="103.478" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_197" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="99.631" x2="101.286" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_198" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="97.419" x2="99.168" y1="210.239" y2="211.73"/>
</g>
</g>
<g id="svg_195">
<line fill="none" id="svg_196" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="101.835" x2="103.478" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_197" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="99.631" x2="101.286" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_198" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="97.419" x2="99.168" y1="210.239" y2="211.73"/>
</g>
</g>
</g>
<g id="svg_199">
<polygon fill="#EDBC9C" id="svg_200" points="132.95599365234375,190.30499267578125 134.1820068359375,208.34300231933594 142.49301147460938,207.98500061035156 142.20401000976562,189.718994140625 "/>
<g id="svg_201">
<g id="svg_202">
<path d="m133.563,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_203"/>
<rect fill="#FFFFFF" height="2.874" id="svg_204" width="27.532" x="116.814" y="215.94"/>
<polygon fill="#EDBC9C" id="svg_200" points="132.95599365234375,190.30499267578125 134.1820068359375,208.34300231933594 142.49301147460938,207.98500061035156 142.20401000976562,189.718994140625 "/>
<g id="svg_201">
<g id="svg_202">
<path d="m133.563,206.34l-15.149,6.908c-0.992,0.498 -1.514,1.611 -1.261,2.692l0,0l26.72,0l-0.993,-9.711l-9.317,0.111z" id="svg_203"/>
<rect fill="#FFFFFF" height="2.874" id="svg_204" width="27.532" x="116.814" y="215.94"/>
</g>
<g id="svg_205">
<line fill="none" id="svg_206" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="128.898" x2="130.54" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_207" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="126.694" x2="128.349" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_208" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="124.482" x2="126.231" y1="210.239" y2="211.73"/>
</g>
</g>
<g id="svg_205">
<line fill="none" id="svg_206" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="128.898" x2="130.54" y1="208.069" y2="209.465"/>
<line fill="none" id="svg_207" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="126.694" x2="128.349" y1="209.129" y2="210.537"/>
<line fill="none" id="svg_208" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.0231" x1="124.482" x2="126.231" y1="210.239" y2="211.73"/>
</g>
</g>
</g>
<g id="svg_209">
<polygon fill="#C4B6A7" id="svg_210" points="110.69400024414062,105.5979995727539 142.58099365234375,108.02799987792969 145.22799682617188,197.60800170898438 129.34298706054688,197.60800170898438 123.68499755859375,130.04200744628906 116.10400390625,158.91400146484375 118.68499755859375,198.6999969482422 103.80899047851562,198.7429962158203 98.67599487304688,154.75100708007812 "/>
<line fill="none" id="svg_211" stroke="#A69382" stroke-miterlimit="10" stroke-width="0.75" x1="116.554" x2="123.685" y1="126.149" y2="130.042"/>
<polygon fill="#C4B6A7" id="svg_210" points="110.69400024414062,105.5979995727539 142.58099365234375,108.02799987792969 145.22799682617188,197.60800170898438 129.34298706054688,197.60800170898438 123.68499755859375,130.04200744628906 116.10400390625,158.91400146484375 118.68499755859375,198.6999969482422 103.80899047851562,198.7429962158203 98.67599487304688,154.75100708007812 "/>
<line fill="none" id="svg_211" stroke="#A69382" stroke-miterlimit="10" stroke-width="0.75" x1="116.554" x2="123.685" y1="126.149" y2="130.042"/>
</g>
</g>
<g id="svg_212">
</g>
<g id="svg_212">
<g id="svg_213">
<polygon fill="#EBAB9D" id="svg_214" points="111.10598754882812,78.26899719238281 111.31298828125,87.06099700927734 106.32199096679688,87.125 106.69000244140625,77.66100311279297 "/>
<path d="m110.799,63.347c0,0 -6.674,4.388 -5.975,15.518l6.489,0.73l-0.514,-16.248z" fill="#808DFF" id="svg_215"/>
<polygon fill="#EBAB9D" id="svg_214" points="111.10598754882812,78.26899719238281 111.31298828125,87.06099700927734 106.32199096679688,87.125 106.69000244140625,77.66100311279297 "/>
<path d="m110.799,63.347c0,0 -6.674,4.388 -5.975,15.518l6.489,0.73l-0.514,-16.248z" fill="#808DFF" id="svg_215"/>
</g>
<g id="svg_216">
<path d="m126.881,58.394c8.026,0 17.123,2.774 17.123,2.774l-0.346,51.05l-33.9,-2.604l0.359,-44.65c0.007,-0.79 0.442,-1.514 1.136,-1.891l9.375,-4.011c0,0.001 4.615,-0.668 6.253,-0.668z" fill="#808DFF" id="svg_217"/>
<line fill="none" id="svg_218" stroke="#667AF1" stroke-miterlimit="10" stroke-width="0.764" x1="109.995" x2="109.995" y1="79.446" y2="68.69"/>
<path d="m126.881,58.394c8.026,0 17.123,2.774 17.123,2.774l-0.346,51.05l-33.9,-2.604l0.359,-44.65c0.007,-0.79 0.442,-1.514 1.136,-1.891l9.375,-4.011c0,0.001 4.615,-0.668 6.253,-0.668z" fill="#808DFF" id="svg_217"/>
<line fill="none" id="svg_218" stroke="#667AF1" stroke-miterlimit="10" stroke-width="0.764" x1="109.995" x2="109.995" y1="79.446" y2="68.69"/>
</g>
<g id="svg_219">
<g id="svg_220">
<path d="m139.987,78.372l2.181,25.251c0,0 2.408,5.078 6.667,-0.204c4.259,-5.282 0.614,-25.916 0.614,-25.916l-9.462,0.869z" fill="#EDBC9C" id="svg_221"/>
<g id="svg_222">
<path d="m137.918,80.866c0,0 -1.819,-18.64 4.081,-19.735c6.117,-1.136 9.67,10.396 10.236,18.659l-14.317,1.076z" fill="#808DFF" id="svg_223"/>
<path d="m140.187,80.695l-2.269,0.171c0,0 -0.664,-6.379 -0.265,-10.813" fill="none" id="svg_224" stroke="#667AF1" stroke-miterlimit="10" stroke-width="0.764"/>
<g id="svg_220">
<path d="m139.987,78.372l2.181,25.251c0,0 2.408,5.078 6.667,-0.204c4.259,-5.282 0.614,-25.916 0.614,-25.916l-9.462,0.869z" fill="#EDBC9C" id="svg_221"/>
<g id="svg_222">
<path d="m137.918,80.866c0,0 -1.819,-18.64 4.081,-19.735c6.117,-1.136 9.67,10.396 10.236,18.659l-14.317,1.076z" fill="#808DFF" id="svg_223"/>
<path d="m140.187,80.695l-2.269,0.171c0,0 -0.664,-6.379 -0.265,-10.813" fill="none" id="svg_224" stroke="#667AF1" stroke-miterlimit="10" stroke-width="0.764"/>
</g>
</g>
</g>
<g id="svg_225">
<g id="svg_226">
<path d="m125.509,79.979c0,0 -1.228,-4.452 -5.684,-4.886c-4.456,-0.434 -5.767,3.064 -4.928,5.368c0.839,2.304 3.738,6.054 8.13,4.136c4.392,-1.918 2.482,-4.618 2.482,-4.618z" fill="#EDBC9C" id="svg_227"/>
<path d="m118.346,75.099c0,0 -4.376,0.502 -7.633,2.089c-0.331,0.161 -0.512,0.525 -0.464,0.89l0,0c0.065,0.493 0.531,0.831 1.02,0.74l6.039,-1.129l1.038,-2.59z" fill="#EDBC9C" id="svg_228"/>
<g id="svg_225">
<g id="svg_226">
<path d="m125.509,79.979c0,0 -1.228,-4.452 -5.684,-4.886c-4.456,-0.434 -5.767,3.064 -4.928,5.368c0.839,2.304 3.738,6.054 8.13,4.136c4.392,-1.918 2.482,-4.618 2.482,-4.618z" fill="#EDBC9C" id="svg_227"/>
<path d="m118.346,75.099c0,0 -4.376,0.502 -7.633,2.089c-0.331,0.161 -0.512,0.525 -0.464,0.89l0,0c0.065,0.493 0.531,0.831 1.02,0.74l6.039,-1.129l1.038,-2.59z" fill="#EDBC9C" id="svg_228"/>
</g>
<g id="svg_229">
<polygon fill="#EDBC9C" id="svg_230" points="143.11099243164062,104.90899658203125 123.0260009765625,84.59700012207031 125.50900268554688,79.97899627685547 145.04400634765625,92.39099884033203 "/>
<line fill="none" id="svg_231" stroke="#DEA888" stroke-miterlimit="10" stroke-width="0.764" x1="140.974" x2="145.044" y1="89.805" y2="92.391"/>
</g>
</g>
<g id="svg_229">
<polygon fill="#EDBC9C" id="svg_230" points="143.11099243164062,104.90899658203125 123.0260009765625,84.59700012207031 125.50900268554688,79.97899627685547 145.04400634765625,92.39099884033203 "/>
<line fill="none" id="svg_231" stroke="#DEA888" stroke-miterlimit="10" stroke-width="0.764" x1="140.974" x2="145.044" y1="89.805" y2="92.391"/>
</g>
</g>
</g>
</g>
<g id="svg_232">
</g>
<g id="svg_232">
<g id="svg_233">
<path d="m98.081,85.09l8.447,25.074c0.29,0.862 1.151,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.042c-0.296,-0.853 -1.151,-1.381 -2.047,-1.264l-20.282,2.664c-1.19,0.157 -1.94,1.361 -1.557,2.498z" fill="#121314" id="svg_234"/>
<polygon fill="#121314" id="svg_235" points="98.44100952148438,83.23100280761719 97.36801147460938,84.6240005493164 129.35501098632812,109.32099914550781 130.50201416015625,107.82599639892578 "/>
<path d="m97.114,86.335l8.447,25.074c0.29,0.862 1.15,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.04c-0.296,-0.853 -1.151,-1.381 -2.047,-1.263l-20.282,2.664c-1.19,0.155 -1.94,1.359 -1.557,2.495z" fill="#455157" id="svg_236"/>
<path d="m109.496,94.445c0.009,1.221 1.005,2.204 2.226,2.195c1.221,-0.009 2.204,-1.005 2.195,-2.226c-0.009,-1.221 -1.005,-2.204 -2.226,-2.195c-1.221,0.008 -2.204,1.005 -2.195,2.226z" fill="#86969C" id="svg_237"/>
<path d="m98.081,85.09l8.447,25.074c0.29,0.862 1.151,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.042c-0.296,-0.853 -1.151,-1.381 -2.047,-1.264l-20.282,2.664c-1.19,0.157 -1.94,1.361 -1.557,2.498z" fill="#121314" id="svg_234"/>
<polygon fill="#121314" id="svg_235" points="98.44100952148438,83.23100280761719 97.36801147460938,84.6240005493164 129.35501098632812,109.32099914550781 130.50201416015625,107.82599639892578 "/>
<path d="m97.114,86.335l8.447,25.074c0.29,0.862 1.15,1.399 2.053,1.28l20.53,-2.697c1.196,-0.157 1.947,-1.373 1.551,-2.513l-8.695,-25.04c-0.296,-0.853 -1.151,-1.381 -2.047,-1.263l-20.282,2.664c-1.19,0.155 -1.94,1.359 -1.557,2.495z" fill="#455157" id="svg_236"/>
<path d="m109.496,94.445c0.009,1.221 1.005,2.204 2.226,2.195c1.221,-0.009 2.204,-1.005 2.195,-2.226c-0.009,-1.221 -1.005,-2.204 -2.226,-2.195c-1.221,0.008 -2.204,1.005 -2.195,2.226z" fill="#86969C" id="svg_237"/>
</g>
<path d="m98.071,93.622l1.923,5.188c0.235,0.633 0.876,1.019 1.545,0.931l0,0c1.97,-0.259 3.232,-2.23 2.642,-4.128l-0.541,-1.74c-0.433,-1.394 -1.754,-2.319 -3.212,-2.25l-1.07,0.051c-0.977,0.047 -1.627,1.03 -1.287,1.948z" fill="#EDBC9C" id="svg_238"/>
</g>
<g id="svg_239">
</g>
<g id="svg_239">
<path d="m119.195,26.137c0,0 19.114,-5.53 12.146,14.151l-3.095,-6.884l-9.051,-7.267z" fill="#412424" id="svg_240"/>
<path d="m111.261,33.648c0,0 -4.703,2.124 0.548,8.693l1.677,-9.011l-2.225,0.318z" fill="#412424" id="svg_241"/>
<path d="m116.028,43.543c0.332,1.727 -0.755,3.394 -2.427,3.723c-1.672,0.329 -3.296,-0.805 -3.628,-2.532c-0.332,-1.727 0.755,-3.394 2.426,-3.723c1.673,-0.33 3.298,0.804 3.629,2.532z" fill="#DEA888" id="svg_242"/>
@@ -202,96 +81,21 @@
<path d="m120.34,57.913c0,0 9.269,-3.364 8.49,-11.668c-0.232,-2.474 -9.473,3.717 -9.473,3.717l0.983,7.951z" fill="#DEA888" id="svg_244"/>
<path d="m122.243,53.961l0,0c-5.105,0.903 -8.527,-3.188 -8.757,-5.275l-2.19,-9.681c-0.788,-4.945 2.364,-9.612 7.123,-10.548l0,0c4.872,-0.958 10.458,2.177 11.425,7.21l0.911,6.449c0.828,5.806 -2.939,10.749 -8.512,11.845z" fill="#EDBC9C" id="svg_245"/>
<g id="svg_246">
<polyline fill="none" id="svg_247" points="117.26400756835938,45.770999908447266 117.29000854492188,47.65800094604492 118.43499755859375,47.56100082397461 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5281"/>
<path d="m121.502,40.326c0,0 -1.907,0.072 -1.828,0.868c0,0 0.068,0.537 1.727,0.441c1.659,-0.096 2.072,-0.273 2.16,-0.796c0.089,-0.524 -1.173,-0.617 -2.059,-0.513z" fill="#1F2326" id="svg_248"/>
<path d="m113.909,41.295c0,0 1.537,-0.358 1.756,0.372c0,0 0.136,0.495 -1.214,0.777c-1.35,0.282 -1.816,0.157 -1.995,-0.234c-0.201,-0.444 0.715,-0.812 1.453,-0.915z" fill="#1F2326" id="svg_249"/>
<path d="m115.497,44.944c0.07,0.45 -0.227,0.87 -0.662,0.939c-0.436,0.069 -0.845,-0.24 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.435,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_250"/>
<path d="m122.407,43.851c0.07,0.45 -0.226,0.87 -0.662,0.939c-0.435,0.068 -0.845,-0.241 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.436,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_251"/>
<path d="m117.256,49.092l4.875,-0.54c0,0 -0.146,1.713 -2.692,1.714c-1.653,0.001 -2.183,-1.174 -2.183,-1.174z" fill="#FFFFFF" id="svg_252"/>
<polyline fill="none" id="svg_247" points="117.26400756835938,45.770999908447266 117.29000854492188,47.65800094604492 118.43499755859375,47.56100082397461 " stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5281"/>
<path d="m121.502,40.326c0,0 -1.907,0.072 -1.828,0.868c0,0 0.068,0.537 1.727,0.441c1.659,-0.096 2.072,-0.273 2.16,-0.796c0.089,-0.524 -1.173,-0.617 -2.059,-0.513z" fill="#1F2326" id="svg_248"/>
<path d="m113.909,41.295c0,0 1.537,-0.358 1.756,0.372c0,0 0.136,0.495 -1.214,0.777c-1.35,0.282 -1.816,0.157 -1.995,-0.234c-0.201,-0.444 0.715,-0.812 1.453,-0.915z" fill="#1F2326" id="svg_249"/>
<path d="m115.497,44.944c0.07,0.45 -0.227,0.87 -0.662,0.939c-0.436,0.069 -0.845,-0.24 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.435,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_250"/>
<path d="m122.407,43.851c0.07,0.45 -0.226,0.87 -0.662,0.939c-0.435,0.068 -0.845,-0.241 -0.915,-0.69c-0.07,-0.45 0.226,-0.87 0.662,-0.939c0.436,-0.069 0.845,0.24 0.915,0.69z" fill="#1F2326" id="svg_251"/>
<path d="m117.256,49.092l4.875,-0.54c0,0 -0.146,1.713 -2.692,1.714c-1.653,0.001 -2.183,-1.174 -2.183,-1.174z" fill="#FFFFFF" id="svg_252"/>
</g>
<path d="m131.754,33.022l-0.413,7.266l-1.408,1.572l0,0c-1.085,-0.37 -2.49,-0.348 -3.389,-1.572c-0.665,-0.904 -1.326,-2.593 -1.349,-3.715l-0.032,-1.564l-3.829,2.827c-0.6,-1.4 -1.279,-3.264 -1.279,-3.264s-0.913,0.065 -2.157,3.101c0,0 -9.875,2.985 -9.87,-3.036c0.005,-6.355 11.168,-8.498 11.168,-8.498l9.609,3.278l2.949,3.605z" fill="#412424" id="svg_253"/>
<g id="svg_254">
<g id="svg_255">
<path d="m135.054,42.129c0.358,1.864 -0.814,3.662 -2.618,4.017c-1.804,0.355 -3.557,-0.868 -3.915,-2.732c-0.358,-1.864 0.814,-3.662 2.618,-4.017c1.805,-0.356 3.557,0.868 3.915,2.732z" fill="#EDBC9C" id="svg_256"/>
<path d="m130.839,43.999c0,0 -0.47,-2.447 1.868,-2.507" fill="none" id="svg_257" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
<g id="svg_255">
<path d="m135.054,42.129c0.358,1.864 -0.814,3.662 -2.618,4.017c-1.804,0.355 -3.557,-0.868 -3.915,-2.732c-0.358,-1.864 0.814,-3.662 2.618,-4.017c1.805,-0.356 3.557,0.868 3.915,2.732z" fill="#EDBC9C" id="svg_256"/>
<path d="m130.839,43.999c0,0 -0.47,-2.447 1.868,-2.507" fill="none" id="svg_257" stroke="#1F2326" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="0.5387"/>
</g>
</g>
<path d="m118.435,60.001l4,-1.86l7.054,0.202l4.137,0.578c0,0 1.542,6.197 -8.92,6.542c-8.365,0.276 -6.271,-5.462 -6.271,-5.462z" fill="#EDBC9C" id="svg_258"/>
</g>
</g>
<g id="svg_259">
<g id="svg_261">
<g id="svg_262">
<g id="svg_264"/>
</g>
</g>
</g>
</g>
<g id="svg_268">
<g id="svg_269">
<g id="svg_270">
<g id="svg_271">
<g id="svg_275"/>
</g>
<g id="svg_281">
<g id="svg_282">
<g id="svg_283">
<g id="svg_284">
<g id="svg_286"/>
</g>
<g id="svg_292"/>
</g>
<g id="svg_295">
<g id="svg_296">
<g id="svg_298"/>
</g>
<g id="svg_304"/>
</g>
</g>
<g id="svg_307">
<g id="svg_308">
<g id="svg_309">
<g id="svg_310"/>
</g>
</g>
<g id="svg_315"/>
<g id="svg_318">
<g id="svg_319"/>
<g id="svg_322">
<g id="svg_323"/>
<g id="svg_326"/>
</g>
</g>
</g>
<g id="svg_329">
<g id="svg_336"/>
<g id="svg_343"/>
</g>
</g>
</g>
<g id="svg_350">
<g id="svg_351">
<g id="svg_352"/>
<g id="svg_355"/>
<g id="svg_358">
<g id="svg_360"/>
</g>
</g>
<g id="svg_363"/>
</g>
</g>
<g id="svg_368">
<g id="svg_370"/>
</g>
</g>
</g>
<g id="DESIGNED_BY_FREEPIK">
<g id="svg_373">
<g id="XMLID_319_">
<g id="XMLID_338_"/>
</g>
<g id="svg_374"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Some files were not shown because too many files have changed in this diff Show More