Compare commits

..

138 Commits

Author SHA1 Message Date
filipefurtad0
7d0bcfa06a Update all locales with the latest Transifex translations 2024-12-13 10:53:25 -06:00
Filipe
9bfac66412 Merge pull request #13012 from chahmedejaz/task/13007-customer-code-in-order-cycle-email
Add customer code to notify producer emails when enabled
2024-12-13 10:50:02 -06:00
Maikel
58c39166e1 Merge pull request #13000 from mkllnk/dfc-amend-order
Amend DFC backorder completely
2024-12-12 12:47:13 +11:00
Maikel Linke
bf41658d32 Fix nil error when amending backorder 2024-12-11 12:40:46 +11:00
Maikel Linke
88837b55b9 Amend backorder also when resuming order 2024-12-11 12:40:46 +11:00
Maikel Linke
9c0a15f431 Amend backorders on admin update orders 2024-12-11 12:40:46 +11:00
Maikel Linke
fcbaefb2c8 Update each backorder only once in bulk cancel 2024-12-11 12:40:46 +11:00
Maikel Linke
9ca1b48d2e Move backorder amendment out of order callback
Triggering it for each order is inefficient when we cancel them in bulk.
The callback doesn't allow us to optimise this.
2024-12-11 12:40:46 +11:00
Maikel Linke
e76d6ad3df Spec current order cancellation amending backorder
The cancellation happens async in Javascript. Therefore we need to wait
for and outcome on the page to know that the action finished. The
expectation needs to be around that whole block.

We actually want only one job enqueued if the same backorder is
affected. Time to fix that.
2024-12-11 12:40:46 +11:00
Maikel Linke
e1febc6e00 Simplify page actions 2024-12-11 12:40:46 +11:00
Maikel Linke
7d27f46d68 Simplify negated expectation 2024-12-11 12:40:46 +11:00
Maikel Linke
7d7253cf0e Re-use BackorderUpdater to complete backorder 2024-12-11 12:40:46 +11:00
Maikel Linke
4fa4eb1b4e Move backorder update code to re-usable class 2024-12-11 12:40:46 +11:00
Maikel Linke
d16cd8c84e Amend backorder completely
Update every single order line to reflect local orders and stock levels.

New cases supported:

* Add lines for orders created by an admin.
* Create backorder line after increase of local line item quantity.
* Adjust local stock after variant has been removed from order cycle.
2024-12-11 12:40:46 +11:00
Gaetan Craig-Riou
9870abfb1c Merge pull request #13025 from openfoodfoundation/dependabot/npm_and_yarn/trix-2.1.10
Bump trix from 2.1.9 to 2.1.10
2024-12-11 10:08:00 +11:00
dependabot[bot]
141000f0df Bump trix from 2.1.9 to 2.1.10
Bumps [trix](https://github.com/basecamp/trix) from 2.1.9 to 2.1.10.
- [Release notes](https://github.com/basecamp/trix/releases)
- [Commits](https://github.com/basecamp/trix/commits)

---
updated-dependencies:
- dependency-name: trix
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 21:47:00 +00:00
Maikel
7302c2d161 Merge pull request #12990 from openfoodfoundation/dependabot/npm_and_yarn/tom-select-2.4.1
Bump tom-select from 2.3.1 to 2.4.1
2024-12-11 08:45:16 +11:00
Ahmed Ejaz
604a47bd96 13007: add specs for text mail 2024-12-10 05:49:35 +05:00
Ahmed Ejaz
241a6d8128 13007: fix specs 2024-12-10 05:34:33 +05:00
Ahmed Ejaz
bb70d21a35 13007: add specs 2024-12-10 05:34:32 +05:00
Ahmed Ejaz
626a269cf8 13007: only show business name when all customers have one 2024-12-10 05:34:32 +05:00
Ahmed Ejaz
302336ab02 13007: add business name in order cycle report email 2024-12-10 05:34:27 +05:00
filipefurtad0
ee2a6bf2e6 Update all locales with the latest Transifex translations 2024-12-09 16:46:51 -06:00
Filipe
7ca5927411 Merge pull request #12996 from chahmedejaz/bugfix/12993-fix-order-cycle-report-text-version
Order cycle mail reports to producers display different data in html and txt versions
2024-12-09 16:32:46 -06:00
dependabot[bot]
2926a9662c Bump tom-select from 2.3.1 to 2.4.1
Bumps [tom-select](https://github.com/orchidjs/tom-select) from 2.3.1 to 2.4.1.
- [Release notes](https://github.com/orchidjs/tom-select/releases)
- [Commits](https://github.com/orchidjs/tom-select/compare/v2.3.1...v2.4.1)

---
updated-dependencies:
- dependency-name: tom-select
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 22:27:56 +00:00
David Cook
118ed915dc Merge pull request #13020 from openfoodfoundation/dependabot/npm_and_yarn/trix-2.1.9
Bump trix from 2.1.8 to 2.1.9
2024-12-10 09:23:28 +11:00
Filipe
6e40e4da60 Merge pull request #13018 from chahmedejaz/task/13008-add-tax-category-in-all-products-report
Add 'tax category' to the All Products report
2024-12-09 16:21:01 -06:00
dependabot[bot]
693bef1e7a Bump trix from 2.1.8 to 2.1.9
Bumps [trix](https://github.com/basecamp/trix) from 2.1.8 to 2.1.9.
- [Release notes](https://github.com/basecamp/trix/releases)
- [Commits](https://github.com/basecamp/trix/commits)

---
updated-dependencies:
- dependency-name: trix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 20:41:14 +00:00
Ahmed Ejaz
42940f4729 12993: add total tax incl. 2024-12-08 01:05:21 +05:00
Ahmed Ejaz
f2da3bb11c 12993: html sanitize city and zip 2024-12-08 01:05:02 +05:00
Ahmed Ejaz
f8003b00db 12993: translate tax incl and qty 2024-12-08 01:03:38 +05:00
Filipe
521b72a6c9 Merge pull request #13004 from chahmedejaz/bugfix/12982-unable-to-close-confirmation-modals
[Orders Page] Fix Confirmation modals do not auto-close
2024-12-07 10:33:56 -06:00
Filipe
aa4552aac4 Merge pull request #13001 from chahmedejaz/bugfix/12973-product-import-never-completes
Bulk product import stalling at 66% when missing info
2024-12-05 16:34:43 -06:00
Ahmed Ejaz
93a3130851 12973: add specs 2024-12-05 16:18:21 -06:00
Ahmed Ejaz
f3a30f94db 12973: fix error rendering on UI
- The caught errors do not get rendered to the UI because of incorrect rendering methods
2024-12-05 16:18:21 -06:00
Ahmed Ejaz
16cae2dbcc 12973: fix error raised in variant creation
- if the sheet doesn't have the units present, then the variant is not saved due to model validation
- After that, while assigning on_hand value, error is raised that the variant is not created first
- Now this commit makes sure that the variant is created before implementing above logic
2024-12-05 16:18:21 -06:00
Maikel
4c71ea3866 Merge pull request #12994 from mkllnk/dfc-update-voc
Add new DFC vocabulary for order states
2024-12-04 12:57:03 +11:00
Gaetan Craig-Riou
bc970927a5 Merge pull request #13017 from openfoodfoundation/dependabot/npm_and_yarn/jasmine-core-5.5.0
Bump jasmine-core from 5.4.0 to 5.5.0
2024-12-04 09:49:37 +11:00
Ahmed Ejaz
c331d57cdb 13008: add tax category in all products report 2024-12-04 03:36:42 +05:00
dependabot[bot]
5845fee663 Bump jasmine-core from 5.4.0 to 5.5.0
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v5.4.0...v5.5.0)

---
updated-dependencies:
- dependency-name: jasmine-core
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-03 09:38:47 +00:00
Maikel
de938f6f10 Merge pull request #12949 from rioug/12859-use-VINE-voucher
[City OFN Voucher] A shopper can use a VINE voucher
2024-12-03 14:04:44 +11:00
Maikel
697f430156 Merge pull request #12992 from mkllnk/errors
Add simpler Alert.raise interface to notify Bugsnag
2024-12-03 13:29:11 +11:00
Maikel Linke
c41c15b895 Fix missed Alert call with order object 2024-12-03 13:02:08 +11:00
Ahmed Ejaz
af200ab4a0 12993: fix lint issues 2024-12-02 18:40:18 +05:00
Ahmed Ejaz
4c6c1eedb1 12993: use html safe strings wherever required 2024-12-02 18:23:24 +05:00
Ahmed Ejaz
bbdee7c0f3 12993: add included tax in text report 2024-12-02 18:04:18 +05:00
Ahmed Ejaz
11959515b8 12993: update the condition to display details 2024-12-02 18:04:17 +05:00
David Cook
cb781536b6 Merge pull request #12905 from macanudo527/docker/use_alpine_image
Add Alpine Image for Docker
2024-12-02 09:57:51 +11:00
Maikel Linke
5719d0682d Remove duplicate lines, dev leftovers 2024-11-29 16:16:43 +11:00
Maikel Linke
c4c95d472e Use defined DFC orders states 2024-11-29 16:16:42 +11:00
Maikel Linke
3e7f61c4d1 Add new DFC vocabulary
So that we can use order states programmatically.
2024-11-29 16:16:42 +11:00
Maikel
db76cd1659 Merge pull request #13005 from mkllnk/map-spec
Remove failing map spec
2024-11-29 14:10:57 +11:00
Maikel Linke
e791184468 Remove failing map spec
I couldn't fix it easily. But I also think that the testing approach had
low value here.

It raised:

```
Failures:

  1) Map map can load does not show alert
     Failure/Error:
       assert_raises(Capybara::ModalNotFound) do
         accept_alert { visit '/map' }
       end

     Minitest::Assertion:
       Capybara::ModalNotFound expected but nothing was raised.

     [Screenshot Image]: /home/runner/work/openfoodnetwork/openfoodnetwork/tmp/capybara/screenshots/failures_r_spec_example_groups_map_map_can_load_does_not_show_alert_64.png

     # ./spec/system/consumer/map_spec.rb:11:in `block (3 levels) in <top (required)>'
     # ./spec/system/support/cuprite_setup.rb:39:in `block (2 levels) in <top (required)>'
     # ./spec/base_spec_helper.rb:153:in `block (3 levels) in <main>'
     # ./spec/base_spec_helper.rb:153:in `block (2 levels) in <main>'
```
2024-11-29 13:46:01 +11:00
filipefurtad0
23287573f4 Update all locales with the latest Transifex translations 2024-11-28 20:21:43 -06:00
Maikel
925ac2ea6a Merge pull request #12862 from dacook/anonymise-customer-names
Anonymise customer first and last names
2024-11-29 09:17:19 +11:00
Filipe
355c9686e3 Merge pull request #12963 from murjax/map-network-check-8230
Show alert if map cannot load
2024-11-28 12:26:22 -06:00
Filipe
58d174fad9 Merge pull request #12969 from chahmedejaz/task/12919-remove-empty-option-from-unit-scale-dropdown
[Products] Empty option on unit scale dropdown
2024-11-28 09:31:42 -06:00
Filipe
d90c4f6aed Merge pull request #12995 from chahmedejaz/bugfix/12968-product-import-update
Impossible to update product sold by weight with product import
2024-11-28 08:38:58 -06:00
Gaetan Craig-Riou
f5b9ca361c Use the voucher adjustment amount for redeeming 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
16d6e1f935 Remove unused error translation 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
1e6fbadd8b Fix Vine::VoucherRedeemerService to handle exceptions 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
d102652c03 Fix Vine::VoucherValidatorService to handle exceptions 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
1b50217242 Re worked the Vine::ApiService to raise exception on error
Log Client and Server error, and re raise exception for the caller
to handle
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
73819a4638 Fix unique validator for vouche code
Paranoia doesn't support unique validation including deleted records:
  https://github.com/rubysherpas/paranoia/pull/333
We use a custom validator, ScopedUniquenessValidator to avoid the issue
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
9ab2a3ae3d Per review, fix a some minor issues 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
d413a142c9 Update various voucher related file to use the new Vouchers::Vine 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
48ad7ed8a0 Add voucher used by multiple enterprise and recycle code scenario
Plus optimise code with `find_or_initialize_by` as suggested in review
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
a2c4c44eea Move Vine voucher to Vouchers::Vine
A Vine voucher is really a specific type of FlatRate voucher but because
a Vine voucher can be used by mutiple enterprise, it can be considered
different enough to warrant it's own class.
It still share a lot of the behaviour of a FlatRate voucher, so to avoid
duplication, all the shared functionality have been moved to a
Vouchers::FlatRatable concern.
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
e7ece294cc Better error for VineVoucherValidatorService
Co-authored-by: David Cook <david@redcliffs.net>
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
d7313ffec9 Per review, improve Vine::VoucherValidatorService
plus specs
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
b42cba8c37 Add vine_voucher factory 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
7726c7d129 Per review, rename not_vine scope to local
- use IS DISTINCT FROM instead of two conditions
- added spec for scope
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
d4d995851f Display voucher section if connected to VINE 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
12cf62c2ff Refactor, add OrderManagement::Order::Updater#update_voucher
Move the logic to update a voucher and associated order to
`OrderManagement`
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
0569b30e0d Refactor Vine related services
Move them under Vine module to keep the code nicely organised
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
9f3da1af4f Reddeem VINE voucher when firing "capture_and_complete_order"o
'Spree::Payment#capture_and_complete!' will try to complete the order,
so we want to redeem any VINE voucher associated with the order first.
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
afb336d789 Add spec for fire event "capture_and_complete_order" 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
92c4cb9b7f Redeem VINE voucher when creating a new payment
Creating a new payment will try to complete the order, so we want to
redeem any VINE voucher associated with the order first
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
724d5a2ca0 Add spec for creating a payment from admin page 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
6251814152 Hide VINE voucher from admin voucher page 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
cf13dc2ff6 Add system spec when completing order with VINE voucher 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
4906c19c8e Checkout Summary, remove shared example 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
129ccc33f8 CheckoutController, add VINE voucher redemption 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
9399c7e129 Add VineVoucherRedeemerService
It handles redeeming a voucher
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
c17eddd69b Add Voucher#vine?
And small refactor
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
b30096317c VineApiService, add voucher_redemptions
It is used to redeem a voucher
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
c89b4fb86b Add system spec fot adding VINE voucher to order 2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
3a367ceb6e Handle adding a VINE voucher to an order
Plus specs
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
f9fb7bf399 Add VineVoucherValidatorService and spec
It handles validating and creating vine voucher
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
0f9b933117 Add extra column to Voucher
They are used to store additional informations for VINE vouchers
2024-11-28 13:35:01 +01:00
Gaetan Craig-Riou
7cbe77668a VineApiService, add voucher_validation
It is used to validate a voucher using the given short code
2024-11-28 13:35:01 +01:00
Rachel Arnould
479eacc956 Merge pull request #12964 from chahmedejaz/task/12890-add-data-to-dfc-affiliate-sales-endpoint
Add cities and countries to DFC affiliate sales data endpoint
2024-11-28 10:46:20 +01:00
Neal Chambers
e7213dba68 Revise README 2024-11-28 08:39:51 +09:00
Neal Chambers
078e191d26 Sync Docker Container to the Host 2024-11-28 08:39:51 +09:00
Neal Chambers
b554eda7c7 Add Windows Docker Commands to README 2024-11-28 08:39:51 +09:00
Neal Chambers
477447ad92 Rewrite Powershell Scripts for Alternative Dockerfile 2024-11-28 08:39:51 +09:00
Neal Chambers
039399ee37 Simplify Docker Build 2024-11-28 08:39:51 +09:00
Neal Chambers
d36438037a Revise Docker README 2024-11-28 08:39:51 +09:00
Neal Chambers
5b58f7b20e Add Dockerfile back in 2024-11-28 08:39:51 +09:00
Neal Chambers
02e2214caa Revert "Remove bundle exec from docker commands"
This reverts commit 2d193a689406cf826e241314acd661fd87a6ae37.
2024-11-28 08:39:51 +09:00
Neal Chambers
0ec8d13641 Optimize Dockerfile 2024-11-28 08:39:51 +09:00
Neal Chambers
151fc7bf85 Revise docker scripts to use optional dockfiles 2024-11-28 08:39:51 +09:00
Neal Chambers
15c920c911 Refactor Alpine Dockerfile 2024-11-28 08:39:51 +09:00
Neal Chambers
b4aaa0fae1 Add Helpful Docker Commands 2024-11-28 08:39:51 +09:00
Neal Chambers
efe0a2a701 Add Redis Test URL for Spree Preferences 2024-11-28 08:39:51 +09:00
Neal Chambers
3f905cce16 Remove bundle exec from docker commands 2024-11-28 08:39:51 +09:00
Neal Chambers
5c5213e872 Use Alpine Image for Docker 2024-11-28 08:39:51 +09:00
David Cook
3a7aed154c Merge pull request #13003 from chahmedejaz/bugfix/13002-orders-pagination
[Orders Page] - Fix Pagination not working
2024-11-28 09:28:30 +11:00
Ahmed Ejaz
60ace5d3ff 12982: add hu locale config for flatpickr 2024-11-28 03:18:34 +05:00
Ahmed Ejaz
1dec3debe1 12982: update close guard condition
- Execute the close method only when the current context modal is opened
2024-11-28 03:12:11 +05:00
Ahmed Ejaz
711f37bce1 13002: fix the search-controller error
- productForm is not accessible on the orders page
- putting the check to do the scrollIntoView only if the productForm is available
2024-11-27 17:49:21 +05:00
Filipe
a493d70f5c Merge pull request #12950 from macanudo527/unlock_bigdecimal
Fix rounding issues by upgrading decimal maths library
2024-11-25 19:18:19 -06:00
Ahmed Ejaz
c0887b1806 12890: remove city from response 2024-11-25 19:29:31 +05:00
Ahmed Ejaz
3d09ac01cc 12968: fix existing specs
- As per the new changes, unit_type change will create a new product rather than give errors.
- on bulk-update screen as well, two products can have same name with different unit_type
2024-11-25 17:29:27 +05:00
Ahmed Ejaz
3a3d729dcb 12968: fix product import update 2024-11-25 16:36:13 +05:00
Ahmed Ejaz
283706114e 12968: update condition to match variant with unit scale 2024-11-25 16:36:13 +05:00
Ahmed Ejaz
7ca544540b 12890: fix product names 2024-11-24 15:24:39 +05:00
Maikel Linke
b1b4b10417 Update API docs 2024-11-22 12:40:54 +05:00
Ahmed Ejaz
3b83200a14 12890: fix specs 2024-11-22 12:40:54 +05:00
Ahmed Ejaz
7cd8311dcb 12890: add cities and countries data 2024-11-22 12:40:54 +05:00
Maikel Linke
14e7c57102 Deactivate some specs on CI
Somehow Bugsnag doesn't report in CI environment and I have no idea how
to circumvent that. And I don't want to spend more time on this.
2024-11-21 16:17:27 +11:00
Maikel Linke
9f859f420d Remove unnecessary error creation 2024-11-21 15:58:56 +11:00
Maikel Linke
af33fc357e Use shorter Alert syntax
I still think that some of these objects won't be visible in Bugsnag but
I don't want to test any more on cases that were broken before and may
not be relevant now.
2024-11-21 15:58:56 +11:00
Maikel Linke
6a8cc410d2 Replace broken order data
Bugsnag expects a string as value, not an ActiveRecord object. The
result was just "filtered" data, not showing any of the order's
attributes.

Since wanting to share the order data seems a common pattern, I added a
convenience method for it.
2024-11-21 15:58:55 +11:00
Maikel Linke
61e7c1db07 Replace obsolete ErrorLogger 2024-11-21 15:58:55 +11:00
Maikel Linke
0d8df5d2a8 Replace Bugsnag calls with Alert.raise 2024-11-21 15:58:55 +11:00
Maikel Linke
73a1698aad Add live test for Bugsnag
Needs human to review Bugsnag account.
2024-11-21 15:28:19 +11:00
Maikel Linke
97d41c230e Add simpler Bugsnag wrapper 2024-11-21 15:28:19 +11:00
Maikel Linke
1b4efd2164 Revert "Notify bugsnag on 404 errors"
This reverts commit 2eec3d625a.

We started tracking 404 errors out of interest without an actual problem
to solve. Now we face rate-limiting in our Bugsnag account. And we
didn't use these 404 reports in the two years this code was active. We
don't even act on all 500 errors. So while our resources are so
constrained, let's keep our focus on the severe errors and user reports
and ignore the rest. 404 errors are mostly generated by vulnerability
scanners.
2024-11-21 15:28:19 +11:00
Maikel Linke
1e13005fb5 Enable Bugsnag testing in any Rails env with ENV var 2024-11-21 15:28:19 +11:00
Neal Chambers
7a5074cc90 Refactor option_value_value_unit_scaled for correct unit value scaling and update sales tax report spec for clarity 2024-11-13 16:28:18 +09:00
Neal Chambers
41ffe848ed Update BigDecimal to Latest Version 2024-11-13 16:28:18 +09:00
Neal Chambers
0797314360 Fix inaccuracies introduced with truncation 2024-11-13 16:28:18 +09:00
Neal Chambers
3302f0e78d Improve Precision of Spec for New Version of BigDecimal 2024-11-13 16:28:18 +09:00
Ahmed Ejaz
bafb881c46 12919: add unit scale prompt 2024-11-10 14:57:45 +05:00
Ryan Murphy
32ab821839 8230 - Map spec update 2024-11-06 08:43:37 -05:00
Ryan Murphy
008d764c34 Show alert if map cannot load 2024-11-05 18:37:17 -05:00
David Cook
9c51615b03 Anonymise customer first and last names
These were added a couple of years ago in https://github.com/openfoodfoundation/openfoodnetwork/pull/8763
But I guess we never noticed the names weren't getting anonymised.

The old 'name' field is still in the DB. It was kept for compatibility during migraiton but never cleaned up. I've added the tech debt task to the welcome new devs board now: https://github.com/openfoodfoundation/openfoodnetwork/issues/8835
2024-09-16 11:42:58 +10:00
148 changed files with 4782 additions and 741 deletions

View File

@@ -34,12 +34,6 @@ Lint/EmptyClass:
Exclude:
- 'spec/lib/reports/report_loader_spec.rb'
# Offense count: 1
# Configuration parameters: AllowComments.
Lint/EmptyFile:
Exclude:
- 'spec/lib/open_food_network/enterprise_injection_data_spec.rb'
# Offense count: 2
Lint/FloatComparison:
Exclude:
@@ -92,7 +86,6 @@ Metrics/AbcSize:
- 'app/controllers/admin/enterprises_controller.rb'
- 'app/controllers/payment_gateways/paypal_controller.rb'
- 'app/controllers/spree/admin/payments_controller.rb'
- 'app/controllers/spree/admin/taxons_controller.rb'
- 'app/controllers/spree/admin/variants_controller.rb'
- 'app/controllers/spree/orders_controller.rb'
- 'app/helpers/spree/admin/navigation_helper.rb'
@@ -127,7 +120,7 @@ Metrics/BlockNesting:
Exclude:
- 'app/models/spree/payment/processing.rb'
# Offense count: 46
# Offense count: 47
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ClassLength:
Exclude:
@@ -137,6 +130,7 @@ Metrics/ClassLength:
- 'app/controllers/admin/resource_controller.rb'
- 'app/controllers/admin/subscriptions_controller.rb'
- 'app/controllers/application_controller.rb'
- 'app/controllers/checkout_controller.rb'
- 'app/controllers/payment_gateways/paypal_controller.rb'
- 'app/controllers/spree/admin/orders_controller.rb'
- 'app/controllers/spree/admin/payment_methods_controller.rb'
@@ -183,7 +177,7 @@ Metrics/ClassLength:
Metrics/CyclomaticComplexity:
Exclude:
- 'app/controllers/admin/enterprises_controller.rb'
- 'app/controllers/spree/admin/taxons_controller.rb'
- 'app/controllers/spree/admin/payments_controller.rb'
- 'app/controllers/spree/orders_controller.rb'
- 'app/helpers/checkout_helper.rb'
- 'app/helpers/order_cycles_helper.rb'
@@ -208,13 +202,12 @@ Metrics/CyclomaticComplexity:
- 'lib/spree/localized_number.rb'
- 'spec/models/product_importer_spec.rb'
# Offense count: 24
# Offense count: 23
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Exclude:
- 'app/controllers/admin/enterprises_controller.rb'
- 'app/controllers/payment_gateways/paypal_controller.rb'
- 'app/controllers/spree/admin/taxons_controller.rb'
- 'app/controllers/spree/orders_controller.rb'
- 'app/helpers/spree/admin/navigation_helper.rb'
- 'app/models/spree/ability.rb'
@@ -293,19 +286,17 @@ Metrics/ParameterLists:
- 'spec/support/controller_requests_helper.rb'
- 'spec/system/admin/reports_spec.rb'
# Offense count: 4
# Offense count: 3
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
Metrics/PerceivedComplexity:
Exclude:
- 'app/controllers/spree/admin/taxons_controller.rb'
- 'app/models/enterprise_relationship.rb'
- 'app/models/spree/ability.rb'
- 'app/models/spree/order/checkout.rb'
# Offense count: 8
# Offense count: 7
Naming/AccessorMethodName:
Exclude:
- 'app/controllers/spree/admin/taxonomies_controller.rb'
- 'app/mailers/producer_mailer.rb'
- 'app/models/spree/order.rb'
- 'app/services/checkout/post_checkout_actions.rb'
@@ -353,7 +344,7 @@ Naming/VariableNumber:
- 'spec/models/spree/tax_rate_spec.rb'
- 'spec/requests/api/orders_spec.rb'
# Offense count: 142
# Offense count: 143
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ResponseMethods.
# ResponseMethods: response, last_response
@@ -557,7 +548,7 @@ RSpecRails/InferredSpecType:
- 'spec/requests/voucher_adjustments_spec.rb'
- 'spec/routing/stripe_spec.rb'
# Offense count: 22
# Offense count: 21
# Configuration parameters: IgnoreScopes, Include.
# Include: app/models/**/*.rb
Rails/InverseOf:
@@ -572,7 +563,6 @@ Rails/InverseOf:
- 'app/models/spree/price.rb'
- 'app/models/spree/product.rb'
- 'app/models/spree/stock_item.rb'
- 'app/models/spree/taxonomy.rb'
- 'app/models/spree/variant.rb'
- 'app/models/subscription_line_item.rb'
@@ -720,7 +710,7 @@ Style/GlobalStdStream:
- 'lib/tasks/subscriptions/debug.rake'
- 'lib/tasks/subscriptions/test.rake'
# Offense count: 12
# Offense count: 10
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowSplatArgument.
Style/HashConversion:
@@ -728,9 +718,7 @@ Style/HashConversion:
- 'app/controllers/admin/column_preferences_controller.rb'
- 'app/controllers/admin/variant_overrides_controller.rb'
- 'app/controllers/spree/admin/products_controller.rb'
- 'app/models/order_cycle.rb'
- 'app/models/product_import/product_importer.rb'
- 'app/models/spree/shipping_method.rb'
- 'app/serializers/api/admin/exchange_serializer.rb'
- 'app/services/variants_stock_levels.rb'
- 'spec/controllers/admin/inventory_items_controller_spec.rb'

View File

@@ -89,4 +89,4 @@ RUN ./script/install-bundler
RUN yarn install
# Run bundler install in parallel with the amount of available CPUs
RUN bundle install --jobs="$(nproc)"
RUN bundle install --jobs="$(nproc)"

View File

@@ -86,7 +86,7 @@ gem "active_model_serializers", "0.8.4"
gem 'activerecord-session_store'
gem 'acts-as-taggable-on'
gem 'angularjs-file-upload-rails', '~> 2.4.1'
gem 'bigdecimal', '3.0.2'
gem 'bigdecimal'
gem 'bootsnap', require: false
gem 'geocoder'
gem 'gmaps4rails'

View File

@@ -180,7 +180,7 @@ GEM
base64 (0.2.0)
bcp47_spec (0.2.1)
bcrypt (3.1.20)
bigdecimal (3.0.2)
bigdecimal (3.1.8)
bindata (2.5.0)
bindex (0.8.1)
bootsnap (1.18.3)
@@ -865,7 +865,7 @@ DEPENDENCIES
angularjs-rails (= 1.8.0)
arel-helpers (~> 2.12)
aws-sdk-s3
bigdecimal (= 3.0.2)
bigdecimal
bootsnap
bugsnag
bullet

32
alpine.Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM ruby:3.1.4-alpine3.19 AS base
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
TZ=Europe/London \
RAILS_ROOT=/usr/src/app
RUN apk --no-cache upgrade && \
apk add --no-cache tzdata postgresql-client imagemagick imagemagick-jpeg && \
apk add --no-cache --virtual wkhtmltopdf
WORKDIR $RAILS_ROOT
# Development dependencies
FROM base AS development-base
RUN apk add --no-cache --virtual .build-deps \
build-base postgresql-dev git nodejs yarn && \
apk add --no-cache --virtual .dev-utils \
bash curl less vim chromium-chromedriver zlib-dev openssl-dev \
readline-dev yaml-dev sqlite-dev libxml2-dev libxslt-dev libffi-dev vips-dev && \
curl -o /usr/local/bin/wait-for-it https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \
chmod +x /usr/local/bin/wait-for-it
# Install yarn dependencies separately for caching
FROM development-base AS yarn-dependencies
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
# Install Ruby gems
FROM development-base
COPY . $RAILS_ROOT
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs "$(nproc)"
COPY --from=yarn-dependencies $RAILS_ROOT/node_modules ./node_modules

View File

@@ -20,10 +20,13 @@ angular.module('Darkswarm').directive 'mapSearch', ($timeout, Search) ->
$timeout =>
map = ctrl.getMap()
searchBox = scope.createSearchBox map
scope.bindSearchResponse map, searchBox
scope.biasResults map, searchBox
scope.performUrlSearch map
if !map
alert(t('gmap_load_failure'))
else
searchBox = scope.createSearchBox map
scope.bindSearchResponse map, searchBox
scope.biasResults map, searchBox
scope.performUrlSearch map
scope.createSearchBox = (map) ->
map.controls[google.maps.ControlPosition.TOP_LEFT].push scope.input

View File

@@ -44,9 +44,9 @@ module Admin
create_connected_app
jwt_service = VineJwtService.new(secret: connected_app_params[:vine_secret])
vine_api = VineApiService.new(api_key: connected_app_params[:vine_api_key],
jwt_generator: jwt_service)
jwt_service = Vine::JwtService.new(secret: connected_app_params[:vine_secret])
vine_api = Vine::ApiService.new(api_key: connected_app_params[:vine_api_key],
jwt_generator: jwt_service)
if !@app.connect(api_key: connected_app_params[:vine_api_key],
secret: connected_app_params[:vine_secret], vine_api:)
@@ -77,7 +77,7 @@ module Admin
def log_and_notify_exception(exception)
Rails.logger.error exception.inspect
Bugsnag.notify(exception)
Alert.raise(exception)
end
def vine_params_empty?

View File

@@ -30,13 +30,13 @@ module Admin
def validate_data
return unless process_data('validate')
render json: @importer.import_results, response: 200
render json: @importer.import_results
end
def save_data
return unless process_data('save')
render json: @importer.save_results, response: 200
render json: @importer.save_results
end
def reset_absent_products
@@ -76,7 +76,7 @@ module Admin
begin
@importer.public_send("#{method}_entries")
rescue StandardError => e
render json: e.message, response: 500
render plain: e.message, status: :internal_server_error
return false
end

View File

@@ -66,7 +66,7 @@ module Api
end
def error_during_processing(exception)
Bugsnag.notify(exception)
Alert.raise(exception)
render(json: { exception: exception.message },
status: :unprocessable_entity) && return

View File

@@ -24,6 +24,7 @@ module Api
Orders::WorkflowService.new(@order).advance_to_payment if @order.line_items.any?
@order.recreate_all_fees!
AmendBackorderJob.perform_later(@order) if @order.completed?
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
end
@@ -73,6 +74,7 @@ module Api
@order.contents.add(variant, quantity, @shipment)
@order.recreate_all_fees!
AmendBackorderJob.perform_later(@order) if @order.completed?
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
end
@@ -86,6 +88,7 @@ module Api
@shipment.reload if @shipment.persisted?
@order.recreate_all_fees!
AmendBackorderJob.perform_later(@order) if @order.completed?
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
end

View File

@@ -52,7 +52,7 @@ module Api
end
def error_during_processing(exception)
Bugsnag.notify(exception)
Alert.raise(exception)
if Rails.env.development? || Rails.env.test?
render status: :unprocessable_entity,

View File

@@ -78,6 +78,18 @@ class CheckoutController < BaseController
return true if redirect_to_payment_gateway
# Redeem VINE voucher
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
unless vine_voucher_redeemer.redeem
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
vine_voucher_redeemer.errors[:redeeming_failed]
else
I18n.t('checkout.errors.voucher_redeeming_error')
end
return false
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
end
@order.process_payments!
@order.confirm!
order_completion_reset @order

View File

@@ -50,9 +50,7 @@ module OrderCompletion
end
def order_invalid!
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
payload.add_metadata :order, :order, @order
end
Alert.raise_with_record("Notice: invalid order loaded during checkout", @order)
flash[:error] = t('checkout.order_not_loaded')
redirect_to main_app.shop_path
@@ -92,9 +90,7 @@ module OrderCompletion
end
def notify_failure(error = RuntimeError.new(order_processing_error))
Bugsnag.notify(error) do |payload|
payload.add_metadata :order, @order
end
Alert.raise_with_record(error, @order)
flash[:error] = order_processing_error if flash.blank?
end

View File

@@ -20,9 +20,7 @@ module OrderStockCheck
def check_order_cycle_expiry
return unless current_order_cycle&.closed?
Bugsnag.notify("Notice: order cycle closed during checkout completion") do |payload|
payload.add_metadata :order, :order, current_order
end
Alert.raise_with_record("Notice: order cycle closed during checkout completion", current_order)
current_order.empty!
current_order.set_order_cycle! nil

View File

@@ -4,11 +4,6 @@ class ErrorsController < ApplicationController
layout "errors"
def not_found
Bugsnag.notify("404") do |event|
event.severity = "info"
event.add_metadata(:request, :env, request.env)
end
render status: :not_found, formats: :html
end

View File

@@ -70,6 +70,7 @@ module Spree
@order.restock_items = params.fetch(:restock_items, "true") == "true"
if @order.public_send(event.to_s)
AmendBackorderJob.perform_later(@order) if @order.completed?
flash[:success] = Spree.t(:order_updated)
else
flash[:error] = Spree.t(:cannot_perform_operation)

View File

@@ -24,9 +24,12 @@ module Spree
end
def create
# Try to redeem VINE voucher first as we don't want to create a payment and complete
# the order if it fails
return redirect_to spree.admin_order_payments_path(@order) unless redeem_vine_voucher
@payment = @order.payments.build(object_params)
load_payment_source
begin
unless @payment.save
redirect_to spree.admin_order_payments_path(@order)
@@ -51,6 +54,10 @@ module Spree
event = params[:e]
return unless event && @payment.payment_source
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
# voucher first and exit if it fails
return if event == "capture_and_complete_order" && !redeem_vine_voucher
# Because we have a transition method also called void, we do this to avoid conflicts.
event = "void_transaction" if event == "void"
if allowed_events.include?(event) && @payment.public_send("#{event}!")
@@ -60,7 +67,7 @@ module Spree
end
rescue StandardError => e
logger.error e.message
Bugsnag.notify(e)
Alert.raise(e)
flash[:error] = e.message
ensure
redirect_to request.referer
@@ -182,6 +189,22 @@ module Spree
%w{capture void_transaction credit refund resend_authorization_email
capture_and_complete_order}
end
def redeem_vine_voucher
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
if vine_voucher_redeemer.redeem == false
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
vine_voucher_redeemer.errors[:redeeming_failed]
else
I18n.t('checkout.errors.voucher_redeeming_error')
end
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
return false
end
true
end
end
end
end

View File

@@ -213,7 +213,7 @@ module Spree
end
def notify_bugsnag(error, product, variant)
Bugsnag.notify(error) do |report|
Alert.raise(error) do |report|
report.add_metadata(:product,
{ product: product.attributes, variant: variant.attributes })
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?

View File

@@ -70,14 +70,15 @@ module Spree
@order.recreate_all_fees! # Enterprise fees on line items and on the order itself
# Re apply the voucher
VoucherAdjustmentsService.new(@order).update
@order.update_totals_and_states
OrderManagement::Order::Updater.new(@order).update_voucher
if @order.complete?
@order.update_payment_fees!
@order.create_tax_charge!
end
AmendBackorderJob.perform_later(@order) if @order.completed?
respond_with(@order) do |format|
format.html do
if params.key?(:checkout)

View File

@@ -1,6 +1,5 @@
# frozen_string_literal: true
require 'open_food_network/error_logger'
require "spree/core/controller_helpers/auth"
require "spree/core/controller_helpers/common"
require "spree/core/controller_helpers/order"
@@ -37,7 +36,7 @@ class UserRegistrationsController < Devise::RegistrationsController
end
end
rescue StandardError => e
OpenFoodNetwork::ErrorLogger.notify(e)
Alert.raise(e)
render_error(message: I18n.t('unknown_error', scope: I18N_SCOPE))
end

View File

@@ -4,7 +4,16 @@ class VoucherAdjustmentsController < BaseController
before_action :set_order
def create
if add_voucher
if voucher_params[:voucher_code].blank?
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
return render_error
end
voucher = load_voucher
return render_error unless valid_voucher?(voucher)
if add_voucher_to_order(voucher)
update_payment_section
elsif @order.errors.present?
render_error
@@ -30,19 +39,28 @@ class VoucherAdjustmentsController < BaseController
@order = current_order
end
def add_voucher
if voucher_params[:voucher_code].blank?
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
return false
end
voucher = Voucher.find_by(code: voucher_params[:voucher_code], enterprise: @order.distributor)
def valid_voucher?(voucher)
return false if @order.errors.present?
if voucher.nil?
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
return false
end
if !voucher.valid?
@order.errors.add(
:voucher_code,
I18n.t(
'checkout.errors.create_voucher_error', error: voucher.errors.full_messages.to_sentence
)
)
return false
end
true
end
def add_voucher_to_order(voucher)
adjustment = voucher.create_adjustment(voucher.code, @order)
unless adjustment.persisted?
@@ -51,14 +69,38 @@ class VoucherAdjustmentsController < BaseController
return false
end
# calculate_voucher_adjustment
clear_payments
VoucherAdjustmentsService.new(@order).update
@order.update_totals_and_states
OrderManagement::Order::Updater.new(@order).update_voucher
true
end
def load_voucher
voucher = Voucher.find_by(code: voucher_params[:voucher_code],
enterprise: @order.distributor)
return voucher unless voucher.nil? || voucher.is_a?(Vouchers::Vine)
vine_voucher
end
def vine_voucher
vine_voucher_validator = Vine::VoucherValidatorService.new(
voucher_code: voucher_params[:voucher_code], enterprise: @order.distributor
)
voucher = vine_voucher_validator.validate
return nil if vine_voucher_validator.errors[:not_found_voucher].present?
if vine_voucher_validator.errors.present?
@order.errors.add(:voucher_code, I18n.t('checkout.errors.add_voucher_error'))
return nil
end
voucher
end
def update_payment_section
render cable_ready: cable_car.replace(
selector: "#checkout-payment-methods",

View File

@@ -1,98 +1,30 @@
# frozen_string_literal: true
# When orders are cancelled, we need to amend
# When orders are created, adjusted or cancelled, we need to amend
# an existing backorder as well.
# We're not dealing with line item changes just yet.
class AmendBackorderJob < ApplicationJob
sidekiq_options retry: 0
def self.schedule_bulk_update_for(orders)
# We can have one backorder per order cycle and distributor.
groups = orders.group_by { |order| [order.order_cycle, order.distributor] }
groups.each_value do |orders_with_same_backorder|
# We need to trigger only one update per backorder.
perform_later(orders_with_same_backorder.first)
end
end
def perform(order)
OrderLocker.lock_order_and_variants(order) do
amend_backorder(order)
end
end
# The following is a mix of the BackorderJob and the CompleteBackorderJob.
# TODO: Move the common code into a re-usable service class.
def amend_backorder(order)
order_cycle = order.order_cycle
distributor = order.distributor
user = distributor.owner
items = backorderable_items(order)
backorder = BackorderUpdater.new.amend_backorder(order)
return if items.empty?
# We are assuming that all variants are linked to the same wholesale
# shop and its catalog:
reference_link = items[0].variant.semantic_links[0].semantic_id
urls = FdcUrlBuilder.new(reference_link)
orderer = FdcBackorderer.new(user, urls)
backorder = orderer.find_open_order(order)
variants = order_cycle.variants_distributed_by(distributor)
adjust_quantities(order_cycle, user, backorder, urls, variants)
FdcBackorderer.new(user, urls).send_order(backorder)
end
# Check if we have enough stock to reduce the backorder.
#
# Our local stock can increase when users cancel their orders.
# But stock levels could also have been adjusted manually. So we review all
# quantities before finalising the order.
def adjust_quantities(order_cycle, user, order, urls, variants)
broker = FdcOfferBroker.new(user, urls)
order.lines.each do |line|
line.quantity = line.quantity.to_i
wholesale_product_id = line.offer.offeredItem.semanticId
transformation = broker.wholesale_to_retail(wholesale_product_id)
linked_variant = variants.linked_to(transformation.retail_product_id)
# Assumption: If a transformation is present then we only sell the retail
# variant. If that can't be found, it was deleted and we'll ignore that
# for now.
next if linked_variant.nil?
# Find all line items for this order cycle
# Update quantity accordingly
if linked_variant.on_demand
release_superfluous_stock(line, linked_variant, transformation)
else
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
end
end
# Clean up empty lines:
order.lines.reject! { |line| line.quantity.zero? }
end
# We look at all linked variants.
def backorderable_items(order)
order.line_items.select do |item|
# TODO: scope variants to hub.
# We are only supporting producer stock at the moment.
item.variant.semantic_links.present?
end
end
def release_superfluous_stock(line, linked_variant, transformation)
# Note that a division of integers dismisses the remainder, like `floor`:
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
# But maybe we didn't actually order that much:
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
line.quantity -= deductable_quantity
retail_stock_changes = deductable_quantity * transformation.factor
linked_variant.on_hand -= retail_stock_changes
end
def aggregate_final_quantities(order_cycle, line, variant, transformation)
orders = order_cycle.orders.invoiceable
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
line.quantity = wholesale_quantity
user = order.distributor.owner
urls = nil # Not needed to send order. The backorder id is the URL.
FdcBackorderer.new(user, urls).send_order(backorder) if backorder
end
end

View File

@@ -19,9 +19,7 @@ class BackorderJob < ApplicationJob
rescue StandardError => e
# Errors here shouldn't affect the checkout. So let's report them
# separately:
Bugsnag.notify(e) do |payload|
payload.add_metadata(:order, :order, order)
end
Alert.raise_with_record(e, order)
end
def perform(order)

View File

@@ -24,8 +24,7 @@ class CompleteBackorderJob < ApplicationJob
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
variants = order_cycle.variants_distributed_by(distributor)
adjust_quantities(order_cycle, user, order, urls, variants)
BackorderUpdater.new.update(order, user, distributor, order_cycle)
FdcBackorderer.new(user, urls).complete_order(order)
@@ -36,55 +35,4 @@ class CompleteBackorderJob < ApplicationJob
raise
end
# Check if we have enough stock to reduce the backorder.
#
# Our local stock can increase when users cancel their orders.
# But stock levels could also have been adjusted manually. So we review all
# quantities before finalising the order.
def adjust_quantities(order_cycle, user, order, urls, variants)
broker = FdcOfferBroker.new(user, urls)
order.lines.each do |line|
line.quantity = line.quantity.to_i
wholesale_product_id = line.offer.offeredItem.semanticId
transformation = broker.wholesale_to_retail(wholesale_product_id)
linked_variant = variants.linked_to(transformation.retail_product_id)
# Assumption: If a transformation is present then we only sell the retail
# variant. If that can't be found, it was deleted and we'll ignore that
# for now.
next if linked_variant.nil?
# Find all line items for this order cycle
# Update quantity accordingly
if linked_variant.on_demand
release_superfluous_stock(line, linked_variant, transformation)
else
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
end
end
# Clean up empty lines:
order.lines.reject! { |line| line.quantity.zero? }
end
def release_superfluous_stock(line, linked_variant, transformation)
# Note that a division of integers dismisses the remainder, like `floor`:
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
# But maybe we didn't actually order that much:
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
line.quantity -= deductable_quantity
retail_stock_changes = deductable_quantity * transformation.factor
linked_variant.on_hand -= retail_stock_changes
end
def aggregate_final_quantities(order_cycle, line, variant, transformation)
orders = order_cycle.orders.invoiceable
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
line.quantity = wholesale_quantity
end
end

View File

@@ -22,11 +22,7 @@ class ReportJob < ApplicationJob
broadcast_result(channel, format, blob) if channel
rescue StandardError => e
Bugsnag.notify(e) do |payload|
payload.add_metadata :report, {
report_class:, user:, params:, format:
}
end
Alert.raise(e, { report: { report_class:, user:, params:, format: } })
broadcast_error(channel)
end

View File

@@ -16,9 +16,7 @@ class StockSyncJob < ApplicationJob
rescue StandardError => e
# Errors here shouldn't affect the shopping. So let's report them
# separately:
Bugsnag.notify(e) do |payload|
payload.add_metadata(:order, :order, order)
end
Alert.raise_with_record(e, order)
end
def self.sync_linked_catalogs_now(order)
@@ -29,9 +27,7 @@ class StockSyncJob < ApplicationJob
rescue StandardError => e
# Errors here shouldn't affect the shopping. So let's report them
# separately:
Bugsnag.notify(e) do |payload|
payload.add_metadata(:order, :order, order)
end
Alert.raise_with_record(e, order)
end
def self.catalog_ids(order)

View File

@@ -55,9 +55,7 @@ class SubscriptionConfirmJob < ApplicationJob
if order.errors.any?
send_failed_payment_email(order)
else
Bugsnag.notify(e) do |payload|
payload.add_metadata :order, :order, order
end
Alert.raise_with_record(e, order)
send_failed_payment_email(order, e.message)
end
end
@@ -108,8 +106,6 @@ class SubscriptionConfirmJob < ApplicationJob
record_and_log_error(:failed_payment, order, error_message)
SubscriptionMailer.failed_payment_email(order).deliver_now
rescue StandardError => e
Bugsnag.notify(e) do |payload|
payload.add_metadata :subscription_data, { order:, error_message: }
end
Alert.raise(e, { subscription_data: { order:, error_message: } })
end
end

View File

@@ -79,14 +79,19 @@ class ProducerMailer < ApplicationMailer
def set_customer_data(line_items)
return unless @coordinator.show_customer_names_to_suppliers?
@display_business_name = false
line_items.map do |line_item|
customer_code = line_item.order.customer&.code
@display_business_name = true if customer_code.present?
{
sku: line_item.variant.sku,
supplier_name: line_item.variant.supplier.name,
product_and_full_name: line_item.product_and_full_name,
quantity: line_item.quantity,
first_name: line_item.order.billing_address.first_name,
last_name: line_item.order.billing_address.last_name
last_name: line_item.order.billing_address.last_name,
business_name: customer_code,
}
end.sort_by { |line_item| [line_item[:last_name].downcase, line_item[:first_name].downcase] }
end

View File

@@ -28,7 +28,7 @@ module Calculator
# In theory it should never be called any more after this has been deployed.
# If the message below doesn't show up in Bugsnag, we can safely delete this method and all
# the related methods below it.
Bugsnag.notify("Calculator::DefaultTax was called with legacy tax calculations")
Alert.raise("Calculator::DefaultTax was called with legacy tax calculations")
calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(order.distributor,
order.order_cycle)

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
require "active_support/concern"
module Vouchers
module FlatRatable
extend ActiveSupport::Concern
included do
validates :amount,
presence: true,
numericality: { greater_than: 0 }
end
def display_value
Spree::Money.new(amount)
end
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
# covers more than the order.total we only need to create an adjustment covering the order.total
def compute_amount(order)
-amount.clamp(0, order.pre_discount_total)
end
def rate(order)
amount = compute_amount(order)
amount / order.pre_discount_total
end
end
end

View File

@@ -481,7 +481,7 @@ class Enterprise < ApplicationRecord
image_variant_url_for(image.variant(name))
rescue StandardError => e
Bugsnag.notify "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
Alert.raise "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
Rails.logger.error(e.message)
nil

View File

@@ -79,10 +79,9 @@ module ProductImport
if entry.attributes['on_hand'].present?
new_variant.on_hand = entry.attributes['on_hand']
end
check_on_hand_nil(entry, new_variant)
end
check_on_hand_nil(entry, new_variant)
if new_variant.valid?
entry.product_object = new_variant
entry.validates_as = 'new_variant' unless entry.errors?
@@ -161,7 +160,7 @@ module ProductImport
end
def unit_fields_validation(entry)
unit_types = ['g', 'oz', 'lb', 'kg', 't', 'ml', 'l', 'kl', '']
unit_types = ['mg', 'g', 'kg', 'oz', 'lb', 't', 'ml', 'cl', 'dl', 'l', 'kl', 'gal', '']
if entry.units.blank?
mark_as_invalid(entry, attribute: 'units',
@@ -297,7 +296,7 @@ module ProductImport
unscaled_units = entry.unscaled_units.to_f || 0
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
if entry.match_inventory_variant?(existing_variant)
if entry.match_variant?(existing_variant)
variant_override = create_inventory_item(entry, existing_variant)
return validate_inventory_item(entry, variant_override)
end

View File

@@ -85,10 +85,6 @@ module ProductImport
end
def match_variant?(variant)
match_display_name?(variant) && variant.unit_value.to_d == unscaled_units.to_d
end
def match_inventory_variant?(variant)
match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d
end

View File

@@ -32,14 +32,18 @@ module ProductImport
def unit_scales
{
'mg' => { scale: 0.001, unit: 'weight' },
'g' => { scale: 1, unit: 'weight' },
'kg' => { scale: 1000, unit: 'weight' },
'oz' => { scale: 28.35, unit: 'weight' },
'lb' => { scale: 453.6, unit: 'weight' },
't' => { scale: 1_000_000, unit: 'weight' },
'ml' => { scale: 0.001, unit: 'volume' },
'cl' => { scale: 0.01, unit: 'volume' },
'dl' => { scale: 0.1, unit: 'volume' },
'l' => { scale: 1, unit: 'volume' },
'kl' => { scale: 1000, unit: 'volume' }
'kl' => { scale: 1000, unit: 'volume' },
'gal' => { scale: 4.54609, unit: 'volume' },
}
end

View File

@@ -34,7 +34,7 @@ module Spree
image_variant_url_for(variant(size))
rescue StandardError => e
Bugsnag.notify "Product ##{viewable_id} Image ##{id} error: #{e.message}"
Alert.raise "Product ##{viewable_id} Image ##{id} error: #{e.message}"
Rails.logger.error(e.message)
self.class.default_image_url(size)

View File

@@ -535,7 +535,7 @@ module Spree
# because an outdated shipping fee is not as bad as a lost payment.
# And the shipping fee is already up-to-date when this error occurs.
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3924
Bugsnag.notify(e) do |report|
Alert.raise(e) do |report|
report.add_metadata(:order, attributes)
report.add_metadata(:shipment, shipment.attributes)
report.add_metadata(:shipment_in_db, Spree::Shipment.find_by(id: shipment.id).attributes)

View File

@@ -142,8 +142,6 @@ module Spree
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
update(payment_state: updater.update_payment_state)
AmendBackorderJob.perform_later(self)
end
def after_resume

View File

@@ -106,7 +106,7 @@ module Spree
calculator.compute(item)
else
# Tax refund should not be possible with the way our production server are configured
Bugsnag.notify(
Alert.raise(
"Notice: Tax refund should not be possible, please check the default zone and " \
"the tax rate zone configuration"
) do |payload|

View File

@@ -293,7 +293,7 @@ module Spree
end
def ensure_unit_value
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
Alert.raise("Trying to set unit_value to NaN") if unit_value&.nan?
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
self.unit_value = 1.0

View File

@@ -15,7 +15,7 @@ class StripeAccount < ApplicationRecord
destroy && Stripe::OAuth.deauthorize(stripe_user_id:)
rescue Stripe::OAuth::OAuthError => e
Bugsnag.notify(
Alert.raise(
e,
stripe_account: stripe_user_id,
enterprise_id:

View File

@@ -48,8 +48,8 @@ class VariantOverride < ApplicationRecord
def move_stock!(quantity)
unless stock_overridden?
Bugsnag.notify RuntimeError.new "Attempting to move stock of a VariantOverride " \
"without a count_on_hand specified."
Alert.raise "Attempting to move stock of a VariantOverride " \
"without a count_on_hand specified."
return
end
@@ -73,8 +73,8 @@ class VariantOverride < ApplicationRecord
self.attributes = { on_demand: false, count_on_hand: default_stock }
save
else
Bugsnag.notify RuntimeError.new "Attempting to reset stock level for a variant " \
"with no default stock level."
Alert.raise "Attempting to reset stock level for a variant " \
"with no default stock level."
end
end
self

View File

@@ -14,7 +14,7 @@ class Voucher < ApplicationRecord
class_name: 'Spree::Adjustment',
dependent: nil
validates :code, presence: true, uniqueness: { scope: :enterprise_id }
validates :code, presence: true
TYPES = ["Vouchers::FlatRate", "Vouchers::PercentageRate"].freeze

View File

@@ -2,24 +2,8 @@
module Vouchers
class FlatRate < Voucher
validates :amount,
presence: true,
numericality: { greater_than: 0 }
include FlatRatable
def display_value
Spree::Money.new(amount)
end
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
# covers more than the order.total we only need to create an adjustment covering the order.total
def compute_amount(order)
-amount.clamp(0, order.pre_discount_total)
end
def rate(order)
amount = compute_amount(order)
amount / order.pre_discount_total
end
validates_with ScopedUniquenessValidator
end
end

View File

@@ -5,6 +5,7 @@ module Vouchers
validates :amount,
presence: true,
numericality: { greater_than: 0, less_than_or_equal_to: 100 }
validates_with ScopedUniquenessValidator
def display_value
ActionController::Base.helpers.number_to_percentage(amount, precision: 2)

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: false
module Vouchers
class Vine < Voucher
include FlatRatable
# a VINE voucher :
# - can potentially be associated with mutiple enterprise
# - code ( "short code" in VINE ) can be recycled, but they shouldn't be linked to the same
# voucher_id
validates :code, uniqueness: { scope: [:enterprise_id, :external_voucher_id] }
end
end

38
app/services/alert.rb Normal file
View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
# A handy wrapper around error reporting libraries like Bugsnag.
#
# Bugsnag's API is great for general purpose but overly complex for our use.
# It also changes over time and we often make mistakes using it. So this class
# aims at:
#
# * Abstracting from Bugsnag, open for other services.
# * Simpler interface to reduce user error.
# * Central place to update Bugsnag API usage when it changes.
#
class Alert
# Alert Bugsnag with additional metadata to appear in tabs.
#
# Alert.raise(
# "Invalid order during checkout",
# {
# order: { number: "ABC123", state: "awaiting_return" },
# env: { referer: "example.com" }
# }
# )
def self.raise(error, metadata = {}, &block)
Bugsnag.notify(error) do |payload|
metadata.each do |name, data|
payload.add_metadata(name, data)
end
block.call(payload)
end
end
def self.raise_with_record(error, record, &)
metadata = {
record.class.name => record&.attributes || { record_was_nil: true }
}
self.raise(error, metadata, &)
end
end

View File

@@ -0,0 +1,147 @@
# frozen_string_literal: true
require 'open_food_network/order_cycle_permissions'
# Update a backorder to reflect all local orders and stock levels
# connected to the associated order cycle.
class BackorderUpdater
# Given an OFN order was created, changed or cancelled,
# we re-calculate how much to order in for every variant.
def amend_backorder(order)
order_cycle = order.order_cycle
distributor = order.distributor
variants = distributed_linked_variants(order_cycle, distributor)
# Temporary code: once we don't need a variant link to look up the
# backorder, we don't need this check anymore.
# Then we can adjust the backorder even though there are no linked variants
# in the order cycle right now. Some variants may have been in the order
# cycle before and got ordered before being removed from the order cycle.
return unless variants.any?
# We are assuming that all variants are linked to the same wholesale
# shop and its catalog:
reference_link = variants[0].semantic_links[0].semantic_id
user = order.distributor.owner
urls = FdcUrlBuilder.new(reference_link)
orderer = FdcBackorderer.new(user, urls)
backorder = orderer.find_open_order(order)
update(backorder, user, distributor, order_cycle)
end
# Update a given backorder according to a distributor's order cycle.
def update(backorder, user, distributor, order_cycle)
variants = distributed_linked_variants(order_cycle, distributor)
# We are assuming that all variants are linked to the same wholesale
# shop and its catalog:
reference_link = variants[0].semantic_links[0].semantic_id
urls = FdcUrlBuilder.new(reference_link)
orderer = FdcBackorderer.new(user, urls)
broker = FdcOfferBroker.new(user, urls)
updated_lines = update_order_lines(backorder, order_cycle, variants, broker, orderer)
unprocessed_lines = backorder.lines.to_set - updated_lines
managed_variants = managed_linked_variants(user, order_cycle, distributor)
cancel_stale_lines(unprocessed_lines, managed_variants, broker)
# Clean up empty lines:
backorder.lines.reject! { |line| line.quantity.zero? }
backorder
end
def update_order_lines(backorder, order_cycle, variants, broker, orderer)
variants.map do |variant|
link = variant.semantic_links[0].semantic_id
solution = broker.best_offer(link)
line = orderer.find_or_build_order_line(backorder, solution.offer)
if variant.on_demand
adjust_stock(variant, solution, line)
else
aggregate_final_quantities(order_cycle, line, variant, solution)
end
line
end
end
def cancel_stale_lines(unprocessed_lines, managed_variants, broker)
unprocessed_lines.each do |line|
wholesale_quantity = line.quantity.to_i
wholesale_product_id = line.offer.offeredItem.semanticId
transformation = broker.wholesale_to_retail(wholesale_product_id)
linked_variant = managed_variants.linked_to(transformation.retail_product_id)
if linked_variant.nil?
transformation.factor = 1
linked_variant = managed_variants.linked_to(wholesale_product_id)
end
# Adjust stock level back, we're not going to order this one.
if linked_variant&.on_demand
retail_quantity = wholesale_quantity * transformation.factor
linked_variant.on_hand -= retail_quantity
end
# We don't have any active orders for this
line.quantity = 0
end
end
def adjust_stock(variant, solution, line)
line.quantity = line.quantity.to_i
if variant.on_hand.negative?
needed_quantity = -1 * variant.on_hand # We need to replenish it.
# The number of wholesale packs we need to order to fulfill the
# needed quantity.
# For example, we order 2 packs of 12 cans if we need 15 cans.
wholesale_quantity = (needed_quantity.to_f / solution.factor).ceil
# The number of individual retail items we get with the wholesale order.
# For example, if we order 2 packs of 12 cans, we will get 24 cans
# and we'll account for that in our stock levels.
retail_quantity = wholesale_quantity * solution.factor
line.quantity += wholesale_quantity
variant.on_hand += retail_quantity
else
# Note that a division of integers dismisses the remainder, like `floor`:
wholesale_items_contained_in_stock = variant.on_hand / solution.factor
# But maybe we didn't actually order that much:
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
if deductable_quantity.positive?
line.quantity -= deductable_quantity
retail_stock_change = deductable_quantity * solution.factor
variant.on_hand -= retail_stock_change
end
end
end
def managed_linked_variants(user, order_cycle, distributor)
# These permissions may be too complex. Here may be scope to optimise.
permissions = OpenFoodNetwork::OrderCyclePermissions.new(user, order_cycle)
permissions.visible_variants_for_outgoing_exchanges_to(distributor)
.where.associated(:semantic_links)
end
def distributed_linked_variants(order_cycle, distributor)
order_cycle.variants_distributed_by(distributor)
.where.associated(:semantic_links)
end
def aggregate_final_quantities(order_cycle, line, variant, transformation)
# We may want to query all these quantities in one go instead of this n+1.
orders = order_cycle.orders.invoiceable
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
line.quantity = wholesale_quantity
end
end

View File

@@ -36,7 +36,7 @@ class FdcBackorderer
.map { |id| find_order(id) }
.compact
# Just in case someone completed the order without updating our database:
.select { |o| o.orderStatus[:path] == "Held" }
.select { |o| o.orderStatus == order_status.HELD }
.first
# The DFC Connector doesn't recognise status values properly yet.
# So we are overriding the value with something that can be exported.
@@ -52,7 +52,7 @@ class FdcBackorderer
def find_last_open_order
graph = import(urls.orders_url)
open_orders = graph&.select do |o|
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
o.semanticType == "dfc-b:Order" && o.orderStatus == order_status.HELD
end
return if open_orders.blank?
@@ -152,7 +152,7 @@ class FdcBackorderer
end
def new?(order)
order.semanticId == urls.orders_url
order.semanticId == urls&.orders_url
end
def build_sale_session(order)
@@ -160,4 +160,10 @@ class FdcBackorderer
session.semanticId = urls.sale_session_url
end
end
private
def order_status
DfcLoader.vocabulary("vocabulary").STATES.ORDERSTATE
end
end

View File

@@ -33,8 +33,7 @@ class FdcOfferBroker
production_flow = catalog_item("#{product_id}/AsPlannedProductionFlow")
if production_flow
wholesale_product_id = production_flow.product
catalog_item(wholesale_product_id)
production_flow.product
else
# We didn't find a wholesale variant, falling back to the given product.
catalog_item(product_id)
@@ -57,7 +56,7 @@ class FdcOfferBroker
consumption_flow = catalog_item(
production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow")
)
retail_product_id = consumption_flow.product
retail_product_id = consumption_flow.product.semanticId
contained_quantity = consumption_flow.quantity.value.to_i
@@ -77,7 +76,7 @@ class FdcOfferBroker
end
def flow_producing(wholesale_product_id)
@production_flows_by_product_id ||= production_flows.index_by(&:product)
@production_flows_by_product_id ||= production_flows.index_by { |flow| flow.product.semanticId }
@production_flows_by_product_id[wholesale_product_id]
end

View File

@@ -15,7 +15,7 @@ module Orders
order.send_cancellation_email = @send_cancellation_email
order.restock_items = @restock_items
order.cancel
end
end.tap { |orders| AmendBackorderJob.schedule_bulk_update_for(orders) }
# rubocop:enable Rails/FindEach
end

View File

@@ -10,6 +10,7 @@ module Orders
return unless order.cancel
Spree::OrderMailer.cancel_email_for_shop(order).deliver_later
AmendBackorderJob.perform_later(order)
end
private

View File

@@ -24,9 +24,7 @@ class PlaceProxyOrder
send_placement_email
rescue StandardError => e
summarizer.record_and_log_error(:processing, order, e.message)
Bugsnag.notify(e) do |payload|
payload.add_metadata :order, :order, order
end
Alert.raise_with_record(e, order)
end
private
@@ -56,9 +54,7 @@ class PlaceProxyOrder
true
rescue StandardError => e
Bugsnag.notify(e) do |payload|
payload.add_metadata(:proxy_order, { subscription:, proxy_order: })
end
Alert.raise(e, { proxy_order: { subscription:, proxy_order: } })
false
end

View File

@@ -145,7 +145,7 @@ module Sets
end
def notify_bugsnag(error, product, variant, variant_attributes)
Bugsnag.notify(error) do |report|
Alert.raise(error) do |report|
report.add_metadata( :product_set,
{ product: product.attributes, variant_attributes:,
variant: variant.attributes } )

View File

@@ -58,7 +58,7 @@ module VariantUnits
def option_value_value_unit_scaled
unit_scale, unit_name = scale_for_unit_value
value = (@nameable.unit_value / unit_scale).to_d.truncate(2)
value = (@nameable.unit_value.to_d / unit_scale).round(2)
[value, unit_name]
end

View File

@@ -0,0 +1,81 @@
# frozen_string_literal: true
require "faraday"
module Vine
class ApiService
attr_reader :api_key, :jwt_generator
def initialize(api_key:, jwt_generator:)
@vine_api_url = ENV.fetch("VINE_API_URL")
@api_key = api_key
@jwt_generator = jwt_generator
end
def my_team
my_team_url = "#{@vine_api_url}/my-team"
call_with_logging do
connection.get(my_team_url)
end
end
def voucher_validation(voucher_short_code)
voucher_validation_url = "#{@vine_api_url}/voucher-validation"
call_with_logging do
connection.post(
voucher_validation_url,
{ type: "voucher_code", value: voucher_short_code },
'Content-Type': "application/json"
)
end
end
def voucher_redemptions(voucher_id, voucher_set_id, amount)
voucher_redemptions_url = "#{@vine_api_url}/voucher-redemptions"
call_with_logging do
connection.post(
voucher_redemptions_url,
{ voucher_id:, voucher_set_id:, amount: amount.to_i },
'Content-Type': "application/json"
)
end
end
private
def connection
jwt = jwt_generator.generate_token
Faraday.new(
request: { timeout: 30 },
headers: {
'X-Authorization': "JWT #{jwt}",
Accept: "application/json"
}
) do |f|
f.request :json
f.response :json
f.request :authorization, 'Bearer', api_key
f.use Faraday::Response::RaiseError
end
end
def call_with_logging
yield
rescue Faraday::ClientError, Faraday::ServerError => e
# caller_location(2,1) gets us the second entry in the stacktrace,
# ie the method where `call_with_logging` is called from
log_error("#{self.class}##{caller_locations(2, 1)[0].label}", e.response)
# Re raise the same exception
raise
end
def log_error(prefix, response)
Rails.logger.error "#{prefix} -- response_status: #{response[:status]}"
Rails.logger.error "#{prefix} -- response: #{response[:body]}"
end
end
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Vine
class JwtService
ALGORITHM = "HS256"
ISSUER = "openfoodnetwork"
def initialize(secret: )
@secret = secret
end
def generate_token
generation_time = Time.zone.now
payload = {
iss: ISSUER,
iat: generation_time.to_i,
exp: (generation_time + 1.minute).to_i,
}
JWT.encode(payload, @secret, ALGORITHM)
end
end
end

View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
module Vine
class VoucherRedeemerService
attr_reader :order, :errors
def initialize(order: )
@order = order
@errors = {}
end
def redeem
# Do nothing if we don't have a vine voucher added to the order
@voucher_adjustment = order.voucher_adjustments.first
@voucher = @voucher_adjustment&.originator
return true if @voucher_adjustment.nil? || !@voucher.is_a?(Vouchers::Vine)
return false if vine_settings.nil?
call_vine_api
@voucher_adjustment.close
true
rescue Faraday::ClientError => e
handle_errors(e.response)
false
rescue Faraday::Error => e
Rails.logger.error e.inspect
Bugsnag.notify(e)
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
false
end
private
def vine_settings
@vine_settings ||= ConnectedApps::Vine.find_by(enterprise: order.distributor)&.data
end
def call_vine_api
jwt_service = Vine::JwtService.new(secret: vine_settings["secret"])
vine_api = Vine::ApiService.new(api_key: vine_settings["api_key"], jwt_generator: jwt_service)
# Voucher adjustment amount is stored in dollars and negative, VINE expect cents
amount = -1 * @voucher_adjustment.amount * 100
vine_api.voucher_redemptions(
@voucher.external_voucher_id, @voucher.external_voucher_set_id, amount
)
end
def handle_errors(response)
if response[:status] == 400
errors[:redeeming_failed] = I18n.t("vine_voucher_redeemer_service.errors.redeeming_failed")
else
errors[:vine_api] = I18n.t("vine_voucher_redeemer_service.errors.vine_api")
end
end
end
end

View File

@@ -0,0 +1,71 @@
# frozen_string_literal: true
module Vine
class VoucherValidatorService
attr_reader :voucher_code, :errors
def initialize(voucher_code:, enterprise:)
@voucher_code = voucher_code
@enterprise = enterprise
@errors = {}
end
def validate
return nil if vine_settings.nil?
response = call_vine_api
save_voucher(response)
rescue Faraday::ClientError => e
handle_errors(e.response)
nil
rescue Faraday::Error => e
Rails.logger.error e.inspect
Bugsnag.notify(e)
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
nil
end
private
def vine_settings
@vine_settings ||= ConnectedApps::Vine.find_by(enterprise: @enterprise)&.data
end
def call_vine_api
# Check voucher is valid
jwt_service = Vine::JwtService.new(secret: vine_settings["secret"])
vine_api = Vine::ApiService.new(api_key: vine_settings["api_key"], jwt_generator: jwt_service)
vine_api.voucher_validation(voucher_code)
end
def handle_errors(response)
if response[:status] == 400
errors[:invalid_voucher] = I18n.t("vine_voucher_validator_service.errors.invalid_voucher")
elsif response[:status] == 404
errors[:not_found_voucher] =
I18n.t("vine_voucher_validator_service.errors.not_found_voucher")
else
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
end
end
def save_voucher(response)
voucher_data = response.body["data"]
# Check if voucher already exist
voucher = Vouchers::Vine.find_or_initialize_by(
code: voucher_code,
enterprise: @enterprise,
external_voucher_id: voucher_data["id"],
external_voucher_set_id: voucher_data["voucher_set_id"]
)
voucher.amount = voucher_data["voucher_value_remaining"].to_f / 100
voucher.save
voucher
end
end
end

View File

@@ -1,39 +0,0 @@
# frozen_string_literal: true
require "faraday"
class VineApiService
attr_reader :api_key, :jwt_generator
def initialize(api_key:, jwt_generator:)
@vine_api_url = ENV.fetch("VINE_API_URL")
@api_key = api_key
@jwt_generator = jwt_generator
end
def my_team
my_team_url = "#{@vine_api_url}/my-team"
jwt = jwt_generator.generate_token
connection = Faraday.new(
request: { timeout: 30 },
headers: {
'X-Authorization': "JWT #{jwt}",
Accept: "application/json"
}
) do |f|
f.request :json
f.response :json
f.request :authorization, 'Bearer', api_key
end
response = connection.get(my_team_url)
if !response.success?
Rails.logger.error "VineApiService#my_team -- response_status: #{response.status}"
Rails.logger.error "VineApiService#my_team -- response: #{response.body}"
end
response
end
end

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
class VineJwtService
ALGORITHM = "HS256"
ISSUER = "openfoodnetwork"
def initialize(secret: )
@secret = secret
end
def generate_token
generation_time = Time.zone.now
payload = {
iss: ISSUER,
iat: generation_time.to_i,
exp: (generation_time + 1.minute).to_i,
}
JWT.encode(payload, @secret, ALGORITHM)
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: false
# paranoia doesn't support unique validation including deleted records:
# https://github.com/rubysherpas/paranoia/pull/333
# We use a custom validator to fix the issue, so we don't need to fork/patch the gem
module Vouchers
class ScopedUniquenessValidator < ActiveModel::Validator
def validate(record)
@record = record
return unless unique_voucher_code_per_enterprise?
record.errors.add :code, :taken, value: @record.code
end
private
def unique_voucher_code_per_enterprise?
query = Voucher.with_deleted.where(code: @record.code, enterprise_id: @record.enterprise_id)
query = query.where.not(id: @record.id) unless @record.id.nil?
query.present?
end
end
end

View File

@@ -3,7 +3,7 @@
= t('.add_new')
%br
- if @enterprise.vouchers.with_deleted.present?
- if @enterprise.vouchers.where.not(type: "Vouchers::Vine").with_deleted.present?
%table
%thead
%tr
@@ -17,7 +17,7 @@
/%th= t('.customers')
/%th= t('.net_value')
%tbody
- @enterprise.vouchers.with_deleted.order(deleted_at: :desc, code: :asc).each do |voucher|
- @enterprise.vouchers.where.not(type: "Vouchers::Vine").with_deleted.order(deleted_at: :desc, code: :asc).each do |voucher|
%tr
%td= voucher.code
%td= voucher.display_value

View File

@@ -12,7 +12,7 @@
= f.hidden_field :variant_unit_scale
= f.select :variant_unit_with_scale,
options_for_select(WeightsAndMeasures.variant_unit_options, variant.variant_unit_with_scale),
{ include_blank: true },
{ include_blank: t('.select_unit_scale') },
{ class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" }, required: true }
= error_message_on variant, :variant_unit, 'data-toggle-control-target': 'control'
.field

View File

@@ -1,5 +1,5 @@
.medium-6#checkout-payment-methods
- if @order.distributor.vouchers.present?
- if @order.distributor.vouchers.present? || @order.distributor.connected_apps.vine.present?
%div.checkout-substep
= render partial: "checkout/voucher_section", locals: { order: @order, voucher_adjustment: @order.voucher_adjustments.first }

View File

@@ -81,6 +81,9 @@
= t :first_name
%th.text-right
= t :last_name
- if @display_business_name
%th.text-right
= t :business_name
%tbody
- @customer_line_items.each do |line_item|
%tr
@@ -97,6 +100,9 @@
= line_item[:first_name]
%td
= line_item[:last_name]
- if @display_business_name
%td
= line_item[:business_name]
%p
= t :producer_mail_text_after
%p

View File

@@ -1,13 +1,13 @@
#{t :producer_mail_greeting} #{@producer.name},
#{t :producer_mail_greeting} #{raw(@producer.name)},
\
= t :producer_mail_text_before
\
- @distributors_pickup_times.each do |distributor_name, pickup_time|
\- #{distributor_name} (#{pickup_time})
\- #{raw(distributor_name)} (#{pickup_time})
\
- if @receival_instructions
= t :producer_mail_delivery_instructions
= @receival_instructions
= raw(@receival_instructions)
\
Orders summary
================
@@ -15,22 +15,22 @@ Orders summary
= t :producer_mail_order_text
\
- @grouped_line_items.each_pair do |product_and_full_name, line_items|
#{line_items.first.variant.sku} - #{raw(line_items.first.variant.supplier.name)} - #{raw(product_and_full_name)} (QTY: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)}
#{line_items.first.variant.sku} - #{raw(line_items.first.variant.supplier.name)} - #{raw(product_and_full_name)} (#{t(:producer_mail_qty)}: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)} (#{t(:with_tax_incl, amount: Spree::Money.new(line_items.sum(&:included_tax), currency: line_items.first.currency))})
\
\
#{t :total}: #{@total}
#{t :total}: #{@total} (#{t(:with_tax_incl, amount: @tax_total)})
\
- if @customer_grouped_line_items
- if @customer_line_items
= t :producer_mail_order_customer_text
\
- @customer_line_items.each do |line_item|
#{line_item[:sku]} - #{raw(line_item[:supplier_name])} - #{raw(line_item[:product_and_full_name])} (QTY: #{line_item[:quantity]}) - #{raw(line_item[:first_name])} #{raw(line_item[:last_name])}
#{line_item[:sku]} - #{raw(line_item[:supplier_name])} - #{raw(line_item[:product_and_full_name])} (#{t(:producer_mail_qty)}: #{line_item[:quantity]}) - #{raw(line_item[:first_name])} #{raw(line_item[:last_name])} #{raw(line_item[:business_name])}
\
\
= t :producer_mail_text_after
#{t :producer_mail_signoff},
#{@coordinator.name}
#{@coordinator.address.address1}, #{@coordinator.address.city}, #{@coordinator.address.zipcode}
#{raw(@coordinator.name)}
#{raw(@coordinator.address.address1)}, #{raw(@coordinator.address.city)}, #{raw(@coordinator.address.zipcode)}
#{@coordinator.phone}
#{@coordinator.contact.email}

View File

@@ -13,6 +13,7 @@ import { ru } from "flatpickr/dist/l10n/ru";
import { sv } from "flatpickr/dist/l10n/sv";
import { tr } from "flatpickr/dist/l10n/tr";
import { en } from "flatpickr/dist/l10n/default.js";
import { hu } from "flatpickr/dist/l10n/hu";
import ShortcutButtonsPlugin from "shortcut-buttons-flatpickr";
import labelPlugin from "flatpickr/dist/plugins/labelPlugin/labelPlugin";
@@ -36,6 +37,7 @@ export default class extends Flatpickr {
sv: sv,
tr: tr,
en: en,
hu: hu,
};
initialize() {

View File

@@ -12,8 +12,8 @@ export const useOpenAndCloseAsAModal = (controller) => {
}.bind(controller),
close: function (_event, remove = false) {
// Only execute close if there is an open modal
if (!document.querySelector("body").classList.contains('modal-open')) return;
// Only execute close if the current modal is open
if (!this.modalTarget.classList.contains('in')) return;
this.modalTarget.classList.remove("in");
this.backgroundTarget.classList.remove("in");

View File

@@ -10,7 +10,8 @@ export default class extends Controller {
changePage(event) {
const productsForm = document.querySelector("#products-form");
productsForm.scrollIntoView({ behavior: "smooth" });
if (productsForm) productsForm.scrollIntoView({ behavior: "smooth" });
this.page.value = event.target.dataset.page;
this.submitSearch();
this.page.value = 1;

View File

@@ -1,5 +1,3 @@
version: '3'
services:
db:
image: postgres:10.19
@@ -24,6 +22,8 @@ services:
- .:/usr/src/app
- gems:/bundles
- ./config/database.yml:/usr/src/app/config/database.yml
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- db
- redis
@@ -32,6 +32,7 @@ services:
OFN_DB_HOST: db
OFN_REDIS_URL: redis://redis/
OFN_REDIS_JOBS_URL: redis://redis
OFN_REDIS_TEST_URL: redis://redis/3
WEBPACKER_DEV_SERVER_HOST: webpack
command: >
bash -c "wait-for-it -t 30 db:5432 &&

View File

@@ -6,5 +6,9 @@ Bugsnag.configure do |config|
config.logger = Logger.new(STDOUT)
config.logger.level = Logger::ERROR
end
config.notify_release_stages = %w(production staging)
# If you want to notify Bugsnag in dev or test then set the env var:
# spring stop
# BUGSNAG=true ./bin/rails console
config.notify_release_stages = %w(production staging) unless ENV["BUGSNAG"]
end

View File

@@ -351,6 +351,8 @@ en:
sku: "SKU"
subtotal: "Subtotal"
tax_rate: "Tax rate"
with_tax_incl: "%{amount} tax incl."
producer_mail_qty: QTY
validators:
date_time_string_validator:
not_string_error: "must be a string"
@@ -574,7 +576,15 @@ en:
depth: "Depth"
payment_could_not_process: "The payment could not be processed"
payment_could_not_complete: "The payment could not be completed"
vine_voucher_validator_service:
errors:
vine_api: "There was an error communicating with the API, please try again later."
invalid_voucher: "The voucher is not valid"
not_found_voucher: "Sorry, we couldn't find that voucher, please check the code."
vine_voucher_redeemer_service:
errors:
vine_api: "There was an error communicating with the API"
redeeming_failed: "Redeeming the voucher failed"
actions:
create_and_add_another: "Create and Add Another"
create: "Create"
@@ -973,6 +983,7 @@ en:
category_field_name: "Category"
tax_category_field_name: "Tax Category"
producer_field_name: "Producer"
select_unit_scale: Select unit scale
clone:
success: Successfully cloned the product
error: Unable to clone the product
@@ -1412,7 +1423,7 @@ en:
connected_apps:
legend: "Connected apps"
affiliate_sales_data:
title: "INRAE Research"
title: "INRAE / UFC QUE CHOISIR Research"
tagline: "Allow this research project to access your orders data anonymously"
enable: "Allow data sharing"
disable: "Stop sharing"
@@ -1420,10 +1431,10 @@ en:
need_to_be_manager: "Only managers can connect apps."
description_html: |
<p>
INRAE are studiying food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project.
INRAE and UFC QUE CHOISIR are teaming up to study food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project.
</p>
<p>
<a href="https://pepr-sams.fr/2024/03/12/plat4terfood/"
<a href="https://apropos.coopcircuits.fr/"
target="_blank"><b>Learn more about this research project</b>
<i class="icon-external-link"></i></a>
</p>
@@ -2123,6 +2134,8 @@ en:
no_shipping_methods_available: Checkout is not possible due to absence of shipping options. Please contact the shop owner.
voucher_not_found: Not found
add_voucher_error: There was an error while adding the voucher
create_voucher_error: "There was an error while creating the voucher: %{error}"
voucher_redeeming_error: There was an error while trying to redeem your voucher
shops:
hubs:
show_closed_shops: "Show closed shops"
@@ -2787,6 +2800,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
confirm_hub_change: "Are you sure? This will change your selected hub and remove any items in your shopping cart."
confirm_oc_change: "Are you sure? This will change your selected order cycle and remove any items in your shopping cart."
location_placeholder: "Type in a location..."
gmap_load_failure: "Unable to load map. Please check your browser settings and allow 3rd party cookies for this website."
error_required: "can't be blank"
error_number: "must be number"
error_email: "must be email address"
@@ -3447,6 +3461,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
ready: Ready
pending: Pending
shipped: Shipped
business_name: Business Name
js:
saving: 'Saving...'
changes_saved: 'Changes saved.'

View File

@@ -107,6 +107,9 @@ en_CA:
count_on_hand:
using_producer_stock_settings_but_count_on_hand_set: "must be blank because you are using producer stock settings"
limited_stock_but_no_count_on_hand: "must be specified because you are forcing limited stock"
connected_apps:
vine:
api_request_error: "An error occured when connecting to Vine API"
messages:
confirmation: "doesn't match %{attribute}"
blank: "can't be blank"
@@ -435,7 +438,7 @@ en_CA:
cancel_order: "Cancel Order"
confirm_send_invoice: "An invoice for this order will be sent to the customer. Are you sure you want to continue?"
confirm_resend_order_confirmation: "Are you sure you want to resend the order confirmation email?"
must_have_valid_business_number: "%{enterprise_name} must have a valid business number before invoices can be used."
must_have_valid_business_number: "%{enterprise_name} must have a valid business number before invoices can be used. Enter a number in enterprise settings."
invoice: "Invoice"
invoices: "Invoices"
file: "File"
@@ -525,6 +528,15 @@ en_CA:
depth: "Depth"
payment_could_not_process: "The payment could not be processed"
payment_could_not_complete: "The payment could not be completed"
vine_voucher_validator_service:
errors:
vine_api: "There was an error communicating with the API, please try again later."
invalid_voucher: "The voucher is not valid"
not_found_voucher: "Sorry, we couldn't find that voucher, please check the code."
vine_voucher_redeemer_service:
errors:
vine_api: "There was an error communicating with the API"
redeeming_failed: "Redeeming the voucher failed"
actions:
create_and_add_another: "Create and Add Another"
create: "Create"
@@ -584,11 +596,11 @@ en_CA:
clone: Clone
delete: Delete
remove: Remove
preview: Prévisualisation
preview: Preview
image:
edit: Edit
product_preview:
product_preview: Prévisualisation du produit
product_preview: Product preview
shop_tab: Shop
product_details_tab: Product details
adjustments:
@@ -606,8 +618,8 @@ en_CA:
first_name: First Name
last_name: Last Name
on_hand: On Hand
on_demand: On Demand
on_demand?: On Demand?
on_demand: Unlimited
on_demand?: Unlimited?
order_cycle: Order Cycle
payment: Payment
payment_method: Payment Method
@@ -717,6 +729,7 @@ en_CA:
connected_apps_enabled:
discover_regen: Discover Regenerative portal
affiliate_sales_data: DFC anonymised orders API for research purposes
vine: Voucher Integration Engine (VINE)
update:
resource: Connected app settings
customers:
@@ -896,6 +909,7 @@ en_CA:
category_field_name: "Category"
tax_category_field_name: "Tax Category"
producer_field_name: "Producer"
select_unit_scale: Select unit scale
clone:
success: Successfully cloned the product
error: Unable to clone the product
@@ -1014,7 +1028,7 @@ en_CA:
variant_unit_name: Variant Unit Name
price: Price
on_hand: On Hand
on_demand: On Demand
on_demand: Unlimited
shipping_category: Shipping Category
tax_category: Tax Category
variant_overrides:
@@ -1097,9 +1111,9 @@ en_CA:
business_details:
legend: "Business Details"
upload: 'upload'
abn: Business Number
abn_placeholder: eg. 80781 2466
acn: Busines Number
abn: Business Number (if applicable)
abn_placeholder: 1234 My Business
acn: GST Number (if applicable)
acn_placeholder: eg.80781 2466
display_invoice_logo: Display logo in invoices
invoice_text: Add customized text at the end of invoices
@@ -1373,9 +1387,25 @@ en_CA:
<i class="icon-external-link"></i></a>
</p>
vine:
title: "Voucher Integration Engine (VINE)"
tagline: "Allow redemption of VINE vouchers in your shopfront."
enable: "Donate"
disable: "Disconnect"
need_to_be_manager: "Only managers can connect apps."
vine_api_key: "VINE API Key"
vine_secret: "VINE secret"
description_html: |
<p>
To enable VINE for your enterprise, enter your API key and secret.
</p>
<p>
<a href="#" target="_blank"><b>VINE</b>
<i class="icon-external-link"></i></a>
</p>
api_parameters_empty: "Please enter an API key and a secret"
api_parameters_error: "Check you entered your API key and secret correctly, contact your instance manager if the error persists"
connection_error: "API connection error, please try again"
setup_error: "VINE API is not configured, please contact your instance manager"
actions:
edit_profile: Settings
properties: Properties
@@ -1672,6 +1702,7 @@ en_CA:
pack_by_customer: Pack By Customer
pack_by_supplier: Pack By Supplier
pack_by_product: Pack By Product
pay_your_suppliers: Pay your suppliers
display:
report_is_big: "This report is big and may slow down your device."
display_anyway: "Display anyway"
@@ -1715,8 +1746,10 @@ en_CA:
name: Xero Invoices
description: Invoices for import into Xero
enterprise_fee_summary:
name: "Enterprise Fee Summary"
name: "All Fees Summary"
description: "Summary of Enterprise Fees collected"
suppliers:
name: Suppliers
enterprise_fees_with_tax_report_by_order: "Enterprise Fees With Tax Report By Order"
enterprise_fees_with_tax_report_by_producer: "Enterprise Fees With Tax Report By Producer"
errors:
@@ -2006,6 +2039,8 @@ en_CA:
no_shipping_methods_available: Checkout is not possible because no shipping/pick-up options are available. Please contact the shop owner.
voucher_not_found: Not found
add_voucher_error: There was an error while adding the voucher
create_voucher_error: "There was an error while creating the voucher: %{error}"
voucher_redeeming_error: There was an error while trying to redeem your voucher
shops:
hubs:
show_closed_shops: "Show closed shops"
@@ -2619,6 +2654,7 @@ en_CA:
confirm_hub_change: "Are you sure? This will change your selected hub and remove any items in your shopping cart."
confirm_oc_change: "Are you sure? This will change your selected order cycle and remove any items in your shopping cart."
location_placeholder: "Type in a location..."
gmap_load_failure: "Unable to load map. Please check your browser settings and allow 3rd party cookies for this website."
error_required: "can't be blank"
error_number: "must be number"
error_email: "must be email address"
@@ -3003,6 +3039,8 @@ en_CA:
report_render_options: Rendering Options
report_header_ofn_uid: OFN UID
report_header_order_cycle: Order Cycle
report_header_order_cycle_start_date: OC Start Date
report_header_order_cycle_end_date: OC End Date
report_header_user: User
report_header_email: Email
report_header_status: Status
@@ -3023,6 +3061,7 @@ en_CA:
report_header_hub_legal_name: "Hub Legal Name"
report_header_hub_contact_name: "Hub Contact Name"
report_header_hub_email: "Hub Public Email"
report_header_hub_contact_email: Hub Contact Email
report_header_hub_owner_email: Hub Owner Email
report_header_hub_phone: "Hub Phone Number"
report_header_hub_address_line1: "Hub Address Line 1"
@@ -3095,6 +3134,8 @@ en_CA:
report_header_producer_suburb: Producer City/Town
report_header_producer_tax_status: Producer Tax Status
report_header_producer_charges_sales_tax?: Tax registered
report_header_producer_abn_acn: Producer ABN/ACN
report_header_producer_address: Producer Address
report_header_unit: Unit
report_header_group_buy_unit_quantity: Group Buy Unit Quantity
report_header_cost: Cost
@@ -3155,7 +3196,11 @@ en_CA:
report_header_total_units: Total Units
report_header_sum_max_total: "Sum Max Total"
report_header_total_excl_vat: "Total excl. tax (%{currency_symbol})"
report_header_total_fees_excl_tax: "Total fees excl. tax (%{currency_symbol})"
report_header_total_tax_on_fees: "Total tax on fees (%{currency_symbol})"
report_header_total: "Total (%{currency_symbol})"
report_header_total_incl_vat: "Total incl. tax (%{currency_symbol})"
report_header_total_excl_fees_and_tax: "Total excl. fees and tax (%{currency_symbol})"
report_header_temp_controlled: TempControlled?
report_header_is_producer: Producer?
report_header_not_confirmed: Not Confirmed

View File

@@ -308,6 +308,8 @@ en_FR:
sku: "SKU"
subtotal: "Subtotal"
tax_rate: "Tax rate"
with_tax_incl: "%{amount} tax incl."
producer_mail_qty: QTY
validators:
date_time_string_validator:
not_string_error: "must be a string"
@@ -528,6 +530,15 @@ en_FR:
depth: "Depth"
payment_could_not_process: "The payment could not be processed"
payment_could_not_complete: "The payment could not be completed"
vine_voucher_validator_service:
errors:
vine_api: "There was an error communicating with the API, please try again later."
invalid_voucher: "The voucher is not valid"
not_found_voucher: "Sorry, we couldn't find that voucher, please check the code."
vine_voucher_redeemer_service:
errors:
vine_api: "There was an error communicating with the API"
redeeming_failed: "Redeeming the voucher failed"
actions:
create_and_add_another: "Create and Add Another"
create: "Create"
@@ -900,6 +911,7 @@ en_FR:
category_field_name: "Category"
tax_category_field_name: "Tax Category"
producer_field_name: "Producer"
select_unit_scale: Select unit scale
clone:
success: Successfully cloned the product
error: Unable to clone the product
@@ -1341,7 +1353,7 @@ en_FR:
connected_apps:
legend: "Connected apps"
affiliate_sales_data:
title: "INRAE / UFC QUE CHOISIR Research"
title: "INRAE Research"
tagline: "Allow this research project to access your orders data anonymously"
enable: "Allow data sharing"
disable: "Stop sharing"
@@ -1349,7 +1361,7 @@ en_FR:
need_to_be_manager: "Only managers can connect apps."
description_html: |
<p>
INRAE and UFC QUE CHOISIR are teaming up to study food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project.
INRAE are teaming up to study food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project.
</p>
<p>
<a href="https://apropos.coopcircuits.fr/"
@@ -1382,7 +1394,7 @@ en_FR:
vine:
title: "Voucher Integration Engine (VINE)"
tagline: "Allow redemption of VINE vouchers in your shopfront."
enable: "About"
enable: "Connect"
disable: "Disconnect"
need_to_be_manager: "Only managers can connect apps."
vine_api_key: "VINE API Key"
@@ -2032,6 +2044,8 @@ en_FR:
no_shipping_methods_available: Checkout is not possible due to absence of shipping options. Please contact the shop owner.
voucher_not_found: Not found
add_voucher_error: There was an error while adding the voucher
create_voucher_error: "There was an error while creating the voucher: %{error}"
voucher_redeeming_error: There was an error while trying to redeem your voucher
shops:
hubs:
show_closed_shops: "Show closed shops"
@@ -2645,6 +2659,7 @@ en_FR:
confirm_hub_change: "Are you sure? This will change your selected hub and remove any items in your shopping cart."
confirm_oc_change: "Are you sure? This will change your selected order cycle and remove any items in your shopping cart."
location_placeholder: "Type in a location..."
gmap_load_failure: "Unable to load map. Please check your browser settings and allow 3rd party cookies for this website."
error_required: "can't be blank"
error_number: "must be number"
error_email: "must be email address"

View File

@@ -3124,7 +3124,7 @@ en_GB:
report_header_producer_suburb: Producer Suburb
report_header_producer_tax_status: Tax Rate Name
report_header_producer_charges_sales_tax?: GST/VAT Registered
report_header_producer_abn_acn: Producer ABN/ACN
report_header_producer_abn_acn: Producer company number
report_header_producer_address: Producer Address
report_header_unit: Unit
report_header_group_buy_unit_quantity: Group Buy Unit Quantity

View File

@@ -1,5 +1,8 @@
es:
language_name: "Español"
time:
formats:
long: "%B %d, %Y %-l:%M %p"
activerecord:
models:
spree/product: Producto
@@ -32,7 +35,7 @@ es:
fee_type: Tipo de Comisión
spree/order:
payment_state: Estado del pago
shipment_state: Estado del envío
shipment_state: Provincia de envío
completed_at: Completado en
number: Número
state: Estado
@@ -50,6 +53,7 @@ es:
primary_taxon: "categoría del producto"
shipping_category_id: "Categoría de envío"
supplier: "Proveedora"
variant_unit: "Valor unidad"
variant_unit_name: "Nombre de la unidad de la variante"
unit_value: "Valor unidad"
spree/credit_card:
@@ -65,10 +69,24 @@ es:
spree/payment_method/calculator:
preferred_flat_percent: "Calculadora Porcentaje fijo:"
preferred_amount: "Calculadora Importe:"
preferred_first_item: "Calculadora Primer elemento:"
preferred_additional_item: "Calculadora Coste del artículo adicional:"
preferred_max_items: "Calculadora Max Items:"
preferred_minimal_amount: "Calculadora Importe mínimo:"
preferred_normal_amount: "Calculadora Cantidad normal:"
preferred_discount_amount: "Calculadora Importe del descuento:"
preferred_unit_from_list: "Calculadora Unidad De la Lista:"
preferred_per_unit: "Calculadora por unidad:"
enterprise:
white_label_logo_link: "Enlace para el logo que se usara en el escaparate"
errors:
models:
enterprise_fee:
inherit_tax_requires_per_item_calculator: "Heredar la categoría fiscal requiere una calculadora por artículo."
spree/image:
attributes:
attachment:
integrity_error: "no se ha podido cargar. Compruebe que el archivo no esté dañado e inténtelo de nuevo."
spree/user:
attributes:
email:
@@ -93,21 +111,39 @@ es:
vine:
api_request_error: "Un error se ha producido al conectarse a Vine API"
messages:
confirmation: "No coincide"
blank: "no puede estar vacío"
too_short: "es demasiado corto (el mínimo es %{count}caracteres)"
errors:
messages:
content_type_invalid: "Tiene un tipo de contenido invalido"
file_size_out_of_range: "el tamaño %{file_size} no está en el rango requerido"
limit_out_of_range: "El número total está fuera de rango"
image_metadata_missing: "no es una imagen valida"
dimension_min_inclusion: "debe ser mayor o igual a %{width} x %{height} píxeles."
dimension_max_inclusion: "debe ser inferior o igual a %{width} x %{height} píxeles."
dimension_width_inclusion: "la anchura no se incluye entre %{min} y %{max} píxel."
dimension_height_inclusion: "la altura no se incluye entre %{min} y %{max} píxel."
dimension_width_greater_than_or_equal_to: "La anchura debe ser mayor o igual a %{length} píxeles."
dimension_height_greater_than_or_equal_to: "La altura debe ser mayor o igual que %{length} píxel."
dimension_width_less_than_or_equal_to: "La anchura debe ser menor o igual a %{length} píxeles."
dimension_height_less_than_or_equal_to: "La altura debe ser menor o igual a %{length} píxel."
dimension_width_equal_to: "La anchura debe ser igual a %{length} píxeles."
dimension_height_equal_to: "la altura debe ser igual a %{length} pixel."
aspect_ratio_not_square: "la imagen tiene que ser cuadrada"
aspect_ratio_not_portrait: "la imagen tiene que ser un retrato"
aspect_ratio_not_landscape: "debe ser una imagen de paisaje"
aspect_ratio_is_not: "debe tener una relación de aspecto de %{aspect_ratio}"
aspect_ratio_unknown: "tiene una relación de aspecto desconocida"
image_not_processable: "no es una imagen valida"
not_found:
title: "La página que estas buscando no existe (404)"
internal_server_error:
title: "Lo sentimos pero algo fue mal (500)"
unprocessable_entity:
title: "El cambio deseado ha sido rechazado (422)"
general_error:
message: "Lo sentimos, pero algo ha ido mal.\n\nPuede que se trate de un problema temporal, así que inténtalo de nuevo o recarga la página.\nRegistramos todos los errores y es posible que estemos trabajando en una solución.\nSi el problema persiste o es urgente, póngase en contacto con nosotras."
stripe:
error_code:
incorrect_number: "El número de tarjeta es incorrecto."
@@ -160,6 +196,7 @@ es:
transaction_not_allowed: "La tarjeta ha sido denegada por una razón desconocida."
try_again_later: "La tarjeta ha sido denegada por una razón desconocida."
withdrawal_count_limit_exceeded: "El cliente ha superado el saldo o límite de crédito disponible en su tarjeta."
disconnect_failure: "No hemos podido desconectar Stripe."
success_code:
disconnected: "Cuenta de Stripe desconectada."
activemodel:
@@ -187,11 +224,16 @@ es:
not_available_to_shop: "no está disponible para %{shop}"
card_details: "Detalles de tarjeta"
card_type: "Tipo de tarjeta"
card_type_is: "El tipo de tarjeta es"
unrecognized_card_type: "Tipo de tarjeta no reconocido"
use_new_cc: "Utilizar otra tarjeta"
what_is_this: "¿Qué es esto?"
cardholder_name: "Nombre del titular de la tarjeta"
community_forum_url: "URL del foro de la comunidad"
customer_instructions: "Instrucciones del Consumidor"
additional_information: "Información adicional"
connect_app:
url: "https://n8n.openfoodnetwork.org/webhook/regen/connect-enterprise"
devise:
passwords:
spree_user:
@@ -228,6 +270,8 @@ es:
updated_not_active: "Su contraseña ha sido restablecida, pero su correo electrónico aún no ha sido confirmado."
updated: "Su contraseña ha sido cambiada con éxito. Ya tienes la sesión iniciada."
send_instructions: "Recibirás un correo electrónico con instrucciones sobre cómo confirmar su cuenta en unos minutos."
oidc:
failure: "No se ha podido iniciar sesión: %{error}"
home_page_alert_html: "HTML de alerta de página de inicio"
hub_signup_case_studies_html: "HTML casos de estudio de registro de hub"
hub_signup_detail_html: "HTML detalle de registro de hub"
@@ -261,6 +305,8 @@ es:
sku: "SKU"
subtotal: "Subtotal"
tax_rate: "% Impuestos"
with_tax_incl: "%{amount}impuestos incluidos"
producer_mail_qty: Cant.
validators:
date_time_string_validator:
not_string_error: "debe ser una cadena"
@@ -268,9 +314,40 @@ es:
integer_array_validator:
not_array_error: "debe ser una matriz"
invalid_element_error: "debe contener solo enteros válidos"
report_job:
report_failed: |
Este informe ha fallado. Puede que sea demasiado grande para procesarlo.
Lo investigaremos, avísenos si el problema persiste.
backorder_mailer:
backorder_failed:
subject: "Ha fallado un pedido pendiente automático"
headline: "Fallo en el pedido pendiente"
description: |
Intentamos realizar o actualizar un pedido pendiente de artículos agotados, pero
algo salió mal. Es posible que tenga existencias negativas y
deba resolver el problema para pedir más existencias.
hints: |
Es posible que tenga que ir a la configuración de OIDC y volver a conectar su cuenta.
Compruebe también que el catálogo de su proveedor no ha cambiado y siguen
disponibles todos los productos que necesita. Póngase en contacto con nosotras si
tiene alguna duda.
order: "Pedido afectado: %{number}"
stock: "Existencias"
product: "Producto"
backorder_incomplete:
subject: "No se ha completado un pedido pendiente automático"
headline: "Su pedido pendiente sigue siendo un borrador"
description: |
Intentamos completar un pedido pendiente de artículos
agotados pero algo salió mal. Las cantidades de pedidos pendientes
pueden ser demasiado altas si ha habido cancelaciones. Su pedido
pendiente no se cumplirá mientras está en estado de borrador.
hints: |
Es posible que tenga que ir a la configuración de OIDC y volver a conectar su cuenta.
Compruebe también que el catálogo de su proveedor no ha cambiado y siguen
disponibles todos los productos que necesita. Póngase en contacto con nosotras si
tiene alguna duda.
affected: "%{enterprise}: %{order_cycle}"
enterprise_mailer:
confirmation_instructions:
subject: "Confirma la dirección de correo electrónico de %{enterprise}"
@@ -296,6 +373,7 @@ es:
heading: "Informe listo para descargar"
intro: |
Este enlace caducara en una semana
link_label: "%{name}"
shipment_mailer:
shipped_email:
dear_customer: "Estimada consumidora,"
@@ -449,6 +527,15 @@ es:
depth: "Profundidad"
payment_could_not_process: "No se pudo procesar el pago"
payment_could_not_complete: "No se pudo completar el pago"
vine_voucher_validator_service:
errors:
vine_api: "Se ha producido un error en la comunicación con la API, inténtelo de nuevo más tarde."
invalid_voucher: "El bono no es válido"
not_found_voucher: "Lo sentimos, no hemos podido encontrar ese vale, por favor, compruebe el código."
vine_voucher_redeemer_service:
errors:
vine_api: "Ha habido un error en la comunicación con el API"
redeeming_failed: "Error al canjear el bono"
actions:
create_and_add_another: "Crear y agregar otro"
create: "Crear"
@@ -469,12 +556,14 @@ es:
filters:
categories:
title: Categorías
selected_categories: "%{count} categorías seleccionadas"
producers:
title: Productoras
colums: Columnas
columns:
image: Imagen
name: Nombre
unit_scale: Escala de unidades
unit: Unidad
unit_value: Valor unidad
display_as: Mostrar unidad como
@@ -504,10 +593,13 @@ es:
clone: Duplicar
delete: Borrar
remove: Eliminar
preview: Vista previa
image:
edit: Editar
product_preview:
product_preview: Vista previa del producto
shop_tab: Tienda
product_details_tab: Detalles del producto
adjustments:
skipped_changing_canceled_order: "No puede cambiar un pedido cancelado."
begins_at: Empieza en
@@ -572,6 +664,7 @@ es:
has_n_rules: "Tiene %{num} reglas"
unsaved_confirm_leave: "Has cambios sin guardar en esta página ¿Continuar sin guardar?"
available_units: "Unidades disponibles"
terms_of_service_have_been_updated_html: "Se han actualizado las condiciones de servicio de Open Food Network:%{tos_link}"
terms_of_service: Leer los términos de servicio
accept_terms_of_service: Aceptar los terminos de servicio
shopfront_settings:
@@ -624,6 +717,12 @@ es:
info_html: "Matomo es una aplicación de análisis web y móvil. Puede alojar Matomo localmente o utilizar un servicio alojado en la nube. Ver <a href='http://matomo.org' target='_blank'>matomo.org</a> para más información."
config_instructions_html: "Aquí puede configurar la integración de OFN Matomo. La siguiente URL de Matomo debe apuntar a la instancia de Matomo a la que se enviará la información de seguimiento del usuario; si se deja vacío, el seguimiento del usuario Matomo se desactivará. El campo ID del sitio no es obligatorio, pero es útil si está rastreando más de un sitio web en una sola instancia de Matomo; se puede encontrar en la consola de la instancia de Matomo."
config_instructions_tag_manager_html: "La configuración de la URL de Matomo Tag Manager habilita Matomo Tag Manager. Esta herramienta le permite configurar eventos analíticos. La URL de Matomo Tag Manager se copia de la sección Código de instalación de Matomo Tag Manager. Asegúrese de seleccionar el contenedor y el entorno correctos, ya que estas opciones cambian la URL."
connected_app_settings:
edit:
title: "Ajustes de la aplicación conectada"
enabled_legend: "Aplicaciones conectadas habilitadas"
update:
resource: Ajustes de la aplicación conectada
customers:
index:
new_customer: "Nuevo Consumidor"
@@ -715,6 +814,7 @@ es:
variants:
infinity: "infinito"
to_order_tip: "Los artículos hechos según demanda no tienen un nivel de stock, como por ejemplo panes hechos según demanda."
back_to_products_list: "Volver a la lista de productos"
editing_product: "Editando producto"
tabs:
product_details: "Detalles del Producto"
@@ -742,6 +842,9 @@ es:
search_for_producers: Buscar productoras
select_producer: Seleccionar productora
all_producers: Todas las productoras
search_for_categories: Buscar por categorías
select_category: Seleccionar categoría
all_categories: Todas las categorías
producers:
label: Productoras
categories:
@@ -762,11 +865,20 @@ es:
new_variant: Nueva variante
bulk_update:
success: Cambios guardados
delete_product:
success: ' El producto ha sido eliminado'
error: No se puede eliminar el producto
delete_variant:
success: ' La variante ha sido eliminada'
error: No se puede eliminar la variante
variant_row:
none_tax_category: Ninguno
category_field_name: "Categoría"
tax_category_field_name: "Categoría de impuestos"
producer_field_name: "Productora"
clone:
success: Se ha clonado el producto
error: No se ha podido clonar el producto
product_import:
title: Importación de productos
file_not_found: Archivo no encontrado o no se pudo abrir
@@ -779,7 +891,9 @@ es:
conditional_blank: no puede estar en blanco si unit_type está en blanco
no_product: no coincide con ningún producto en la base de datos
not_found: no encontrado en la base de datos
category_not_found: no coincide con las categorías permitidas. Consulta las categorías correctas para elegir en la página de importación de productos o comprueba que no haya ningún error ortográfico.
not_updatable: No se puede actualizar sobre productos existentes a través de la importación de productos
values_must_be_same: debe ser el mismo para los productos con el mismo nombre
blank: no puede estar vacío
products_no_permission: no tienes permiso para administrar productos para esta organización
inventory_no_permission: no tienes permiso para crear inventario para esta productora
@@ -809,6 +923,7 @@ es:
dfc_import_form:
title: "Importar del catálogo DFC"
enterprise: "Organización"
catalog_url: "URL del catálogo DFC"
import: "Importar"
import:
review: Revisión
@@ -911,6 +1026,7 @@ es:
orders:
edit:
order_sure_want_to: ¿Está seguro de que desea %{event} este pedido?
tax_on_fees: "Impuesto sobre las tasas"
invoice_email_sent: 'Se ha enviado correo electrónico con la factura.'
order_email_resent: 'El correo electrónico del pedido se ha reenviado'
bulk_management:
@@ -996,6 +1112,8 @@ es:
phone: Teléfono
phone_placeholder: ej. 98 7654 3210
whatsapp_phone: Número de teléfono WhatsApp
whatsapp_phone_placeholder: e. +34 666 666666
whatsapp_phone_tip: "Este número se mostrará en tu perfil público para que se abra como un enlace de WhatsApp."
website: Website
website_placeholder: ej. www.truffles.com
enterprise_fees:
@@ -1062,7 +1180,7 @@ es:
permalink:
permalink: Permalink (sin espacios)
permalink_tip: "Se usa para crear la URL de tu tienda: %{link}nombre-de-tu-tienda/shop"
link_to_front: Link a la tienda
link_to_front: Enlace a la tienda
link_to_front_tip: Enlace directo a tu tienda en Open Food Networks
ofn_uid: UID de OFN
ofn_uid_tip: La identificación única utilizada para identificar la organización en Open Food Network.
@@ -1119,6 +1237,7 @@ es:
shopfront_sort_by_producer: "Por productora"
shopfront_sort_by_category_placeholder: "Categoría"
shopfront_sort_by_producer_placeholder: "Productora"
display_remaining_stock: "Mostrar las existencias restantes en el escaparate si hay pocas disponibles"
display_remaining_stock_tip: "Informe a los compradores cuando solo queden 3 artículos o menos."
enabled: "Habilitado"
disabled: "Deshabilitado"
@@ -1175,25 +1294,42 @@ es:
voucher_code: Código promocional
rate: Impuesto
label: Etiqueta
purpose: Propósito
expiry: Caducidad
use_limit: Utilización/Límite
customers: Consumidora
net_value: Valor neto
active: ¿Activo?
add_new: Añadir nuevo
no_voucher_yet: Todavía no hay bonos
white_label:
legend: "Etiqueta blanca"
hide_ofn_navigation: "Ocultar la navegación de OFN"
upload_logo: "Logo que se utilizará en el escaparate"
remove_logo: "Eliminar el logo"
remove_logo_confirm: "¿Está seguro de que desea eliminar este logo?"
remove_logo_success: "Logo eliminado"
white_label_logo_link_label: "Enlace para el log que se usara en el escaparate"
hide_groups_tab: "Ocultar la pestaña de grupos en el escaparate"
create_custom_tab: "Crear pestaña personalizada en el escaparate"
custom_tab_title: "Título de la pestaña personalizada"
custom_tab_content: "Contenido de la ficha personalizada"
connected_apps:
legend: "Aplicaciones connectadas"
affiliate_sales_data:
enable: "Permitir compartir datos"
disable: "Dejar de compartir"
loading: "Cargando"
need_to_be_manager: "Sólo los administradores pueden conectar aplicaciones."
discover_regen:
enable: "Permitir compartir datos"
disable: "Dejar de compartir"
loading: "Cargando"
need_to_be_manager: "Sólo los administradores pueden conectar aplicaciones."
vine:
enable: "Conectar"
disable: "Desconectar"
need_to_be_manager: "Sólo los administradores pueden conectar aplicaciones."
actions:
edit_profile: Configuración
properties: Propiedades
@@ -1246,8 +1382,10 @@ es:
contact_name: Nombre de Contacto
edit:
editing: 'Configuración:'
back_link: Volver a la lista de organizaciones
new:
title: Nueva Organización
back_link: Volver a la lista de organizaciones
welcome:
welcome_title: Bienvenida a Open Food Network!
welcome_text: Has creado correctamente un
@@ -1285,6 +1423,7 @@ es:
re_notify_producers: Re notificar a los productores
notify_producers_tip: Esto le enviará un correo a cada productor con su lista de pedidos.
date_time_warning_modal_content:
proceed: 'Proceda de todos modos'
cancel: 'Cancelar'
incoming:
incoming: "Entrante"
@@ -1412,6 +1551,7 @@ es:
could_not_resume_the_order: No se pudo reanudar el pedido
select2:
searching: Buscando...
no_matches: No se han encontrado coincidencias
shared:
user_guide_link:
user_guide: Manual de Usuario
@@ -1419,6 +1559,8 @@ es:
has_no_payment_methods: "%{enterprise} no tiene formas de pago"
has_no_shipping_methods: "%{enterprise} no tiene ningún método de envío"
has_no_enterprise_fees: "%{enterprise} no tiene comisiones de organización"
flashes:
dismiss: Desestimar
side_menu:
enterprise:
primary_details: "Detalles Principales"
@@ -1438,6 +1580,8 @@ es:
shop_preferences: "Configuración de la tienda"
users: "Usuarias"
vouchers: Bonos
white_label: "Etiqueta blanca"
connected_apps: "Aplicaciones connectadas"
enterprise_group:
primary_details: "Detalles Principales"
users: "Usuarias"
@@ -1473,10 +1617,14 @@ es:
pack_by_customer: Pack por Consumidor
pack_by_supplier: Pack por proveedor
pack_by_product: Empaquetar por producto
display:
display_anyway: "Mostrar de todos modos"
download:
button: "Descargar informe"
show:
report_link_label: Descargar informe (si está disponible)
revenues_by_hub:
description: Ingresos por grupo
orders_and_distributors:
name: Pedidos y Distribuidores
description: Pedidos con detalles de distribuidor
@@ -1505,6 +1653,8 @@ es:
enterprise_fee_summary:
name: "Resumen de las comisiones de la organización"
description: "Resumen de las comisiones de la organización recolectadas"
suppliers:
name: Proveedoras
errors:
no_report_type: "Por favor especifique un tipo de reporte"
report_not_found: "El reporte no fue encontrado"
@@ -1540,6 +1690,7 @@ es:
oidc_settings:
index:
connect: "Conecta tu cuenta"
disconnect: "Desconectar"
view_account: "Para ver tu cuenta, ver:"
subscriptions:
index:
@@ -1810,7 +1961,7 @@ es:
login: "login"
contact: "contactar"
require_customer_login: "Sólo las consumidoras aprobadas pueden acceder a esta tienda."
require_login_2_html: "¿Quieres comprar en esta tienda? %{contact}%{enterprise}y solicita tu admisión."
require_login_2_html: "¿Quieres comprar en esta tienda? %{contact} %{enterprise} y solicita tu admisión."
require_customer_html: "Si desea comenzar a comprar aquí, por favor %{contact} %{enterprise} para preguntar acerca de incorporación."
select_oc:
select_oc_html: "Por favor <span class='highlighted'>debes elegir cuando quieres tu pedido</span>, para poder ver qué productos hay disponibles."
@@ -2051,6 +2202,7 @@ es:
order_back_to_store: Volver a la Tienda
order_back_to_cart: Volver al Carrito
order_back_to_website: Volver al sitio web
checkout_summary_title: Resumen de caja
bom_tip: "Usa esta página para alterar la cantidad del producto en varios pedidos a la vez. Los productos pueden ser eliminados de los pedidos si es necesario. "
unsaved_changes_warning: "Hay cambios no guardados, se perderán si continúas."
unsaved_changes_error: "Los campos con bordes rojos contienen errores."
@@ -2330,6 +2482,7 @@ es:
orders_bought_already_confirmed: "* ya confirmados"
orders_confirm_cancel: "Seguro que desea cancelar este pedido?"
order_processed_successfully: "Su pedido ha sido procesado con éxito."
thank_you_for_your_order: "Gracias por su pedido"
products_cart_distributor_choice: "Distribuidora para tu pedido:"
products_cart_distributor_change: "Su distribuidora para este pedido se cambiará a %{name} si agregas este producto al carrito."
products_cart_distributor_is: "Tu distribuidora para este pedido es %{name}."
@@ -2447,6 +2600,7 @@ es:
contact_field_required: "Debes introducir un contacto principal."
phone_field: "Número de teléfono"
whatsapp_phone_field: "Número de teléfono WhatsApp"
whatsapp_phone_tooltip: "Este número se mostrará en tu perfil público para que se abra como un enlace de WhatsApp."
phone_field_placeholder: "p.ej. 93 250 16 45"
type:
title: "Tipo"
@@ -2765,6 +2919,10 @@ es:
report_header_hub_contact_name: "Nombre de contacto del grupo"
report_header_hub_email: "Correo electrónico público del grupo"
report_header_hub_owner_email: Correo electrónico del responsable de grupo
report_header_hub_address_line1: "Dirección 1 del grupo"
report_header_hub_address_line2: "Dirección 2 del grupo"
report_header_hub_address_zipcode: "Código postal del grupo"
report_header_hub_address_state_name: "Región del grupo"
report_header_code: Código
report_header_paid: ¿Pagado?
report_header_delivery: ¿Entregado?
@@ -2775,7 +2933,7 @@ es:
report_header_ship_street_2: Calle de envío 2
report_header_ship_city: Ciudad de envío
report_header_ship_postcode: Código postal de envío
report_header_ship_state: Estado del envío
report_header_ship_state: Provincia de envío
report_header_billing_street: Calle de facturación
report_header_billing_street_2: Calle de facturación 2
report_header_billing_street_3: Calle de facturación 3
@@ -2827,6 +2985,7 @@ es:
report_header_supplier: Proveedor
report_header_producer: Productora
report_header_producer_suburb: Barrio Productora
report_header_producer_address: Dirección de la productora
report_header_unit: Unidad
report_header_group_buy_unit_quantity: Agrupar por cantidad unidad
report_header_cost: Coste
@@ -2900,6 +3059,8 @@ es:
report_header_transaction_fee: Comisión por transacción (sin impuestos)
report_header_total_untaxable_admin: Total de ajustes administrativos no tributables (sin impuestos)
report_header_total_taxable_admin: Total de ajustes tributarios de administración (impuestos incluidos)
report_header_voucher_label: Etiqueta del bono
report_header_voucher_amount: "Cantidad del bono (%{currency_symbol})"
invoice_date: "Fecha de factura"
due_date: "Fecha de vencimiento"
account_code: "Código de cuenta"
@@ -3209,6 +3370,7 @@ es:
processing: "procesando"
void: "pendiente"
invalid: "inválido"
quantity_unavailable: "Existencias insuficientes. ¡Artículo no guardado!"
quantity_unchanged: "Cantidad sin cambios respecto a la cantidad anterior."
resend_user_email_confirmation:
resend: "Reenviar"
@@ -3344,7 +3506,9 @@ es:
link: "Enlace"
numbers: "Numeros"
redo: "Rehacer"
strike: "Tachado"
undo: "Deshacer"
unlink: "Desenlazar"
url: "URL"
urlPlaceholder: "Por favor introduzca una URL para insertar"
inflections:
@@ -3584,6 +3748,7 @@ es:
resend: "Reenviar"
back_to_orders_list: "Volver a la lista de pedidos"
back_to_payments_list: "Volver a la lista de pagos"
back_to_states_list: "Volver a la lista de regiones"
return_authorizations: "Autorizaciones de devolución"
cannot_create_returns: "No se pueden crear devoluciones ya que este pedido no tiene unidades enviadas."
select_stock: "Seleccionar stock"
@@ -3628,6 +3793,7 @@ es:
credit_card: "Tarjeta de crédito"
new_payment: "Nuevo pago"
capture: "Pagado"
capture_and_complete_order: "Capturar y completar el pedido"
void: "Pendiente"
login: "Iniciar sesión"
password: "Contraseña"
@@ -3671,6 +3837,7 @@ es:
tax_rate_amount_explanation: "Las tasas de impuestos son una cantidad decimal para ayudar en los cálculos (es decir, si la tasa de impuestos es del 5%, introduzca 0.05)"
included_in_price: "Incluido en el precio"
show_rate_in_label: "Mostrar impuesto en la etiqueta"
back_to_tax_rates_list: "Volver a la lista de impuestos"
tax_settings: "Configuración de Impuestos"
zones: "Zonas"
new_zone: "Nueva zona"
@@ -3683,6 +3850,7 @@ es:
iso_name: "Nombre ISO"
states_required: "Estados requeridos"
editing_country: "Editar país"
back_to_countries_list: "Volver a la lista de países"
states: "Estados"
abbreviation: "Abreviatura"
new_state: "Nuevo estado"
@@ -3696,6 +3864,7 @@ es:
shipping_categories: "Categorías de envío"
new_shipping_category: "Nueva categoría de envío"
back_to_shipping_categories: "Volver a las categorías de envío"
editing_shipping_category: "Edición de la categoría de envío"
name: "Nombre"
description: "Descripción"
type: "Tipo"
@@ -3894,7 +4063,7 @@ es:
one: "1 pedido seleccionado"
sortable_header:
payment_state: "Estado del pago"
shipment_state: "Estado del envío"
shipment_state: "Provincia de envío"
completed_at: "Completado en"
number: "Número"
state: "Estado"
@@ -3948,6 +4117,8 @@ es:
many: "Tienes %{count} ciclos de pedido activos."
other: "Tienes %{count} ciclos de pedido activos."
manage_order_cycles: "GESTIONA LOS CICLOS DE PEDIDO"
version:
view_all_releases: Ver todas las publicaciones
shipping_methods:
index:
shipping_methods: "Métodos de envío"
@@ -4074,6 +4245,7 @@ es:
product_name: nombre del producto
primary_taxon_form:
product_category: categoría del producto
search_for_categories: "Buscar por categorias"
group_buy_form:
group_buy: "¿Agrupado por?"
bulk_unit_size: Tamaño de la unidad a granel
@@ -4097,11 +4269,13 @@ es:
back_to_users_list: "Volver a la lista de usuarias"
general_settings: "Configuración general"
form:
disabled: "¿Deshabilitado?"
email: "Email"
roles: "Roles"
enterprise_limit: "Límite de la Organización"
confirm_password: "Confirmar contraseña"
password: "Contraseña"
locale: "Idioma"
email_confirmation:
confirmation_pending: "La confirmación por correo electrónico está pendiente. Hemos enviado un correo electrónico de confirmación a %{address}."
variants:
@@ -4125,6 +4299,7 @@ es:
display_name: "Nombre para mostrar"
display_as_placeholder: 'p. ej. 2 Kg'
display_name_placeholder: 'p. ej. Tomates'
unit_scale: "Escala de unidades"
unit: Unidad
price: Precio
unit_value: Valor unidad
@@ -4142,13 +4317,19 @@ es:
completed_at: "Completado en"
state: "Provincia"
payment_state: "Estado del pago"
shipment_state: "Estado del envío"
shipment_state: "Provincia de envío"
email: "Email"
total: "Total"
billing_address_name: "Nombre"
taxons:
back_to_list: "Volver a la lista de categoría de producto"
index:
title: "Categorías de Producto"
new_taxon: 'Nueva categoría de producto'
new:
title: "Nueva categoría de producto"
edit:
title: "Editar la categoría del producto"
form:
name: Nombre
description: Descripción
@@ -4261,6 +4442,7 @@ es:
thanks: "Gracias por hacer negocios."
track_information: "Información de seguimiento: %{tracking}"
track_link: "Enlace de seguimiento: %{url}"
picked_up_subject: "Notificación de recogida"
test_mailer:
test_email:
greeting: "¡Felicidades!"
@@ -4309,6 +4491,7 @@ es:
api_keys:
regenerate_key: "Regenerar llave"
webhook_endpoints:
description: Los eventos del sistema pueden desencadenar webhooks a sistemas externos.
event_types:
order_cycle_opened: Ciclo de pedidos abierto
developer_settings:
@@ -4448,3 +4631,4 @@ es:
previous: Anterior
invisible_captcha:
sentence_for_humans: "Por favor, dejalo vacio"
timestamp_error_message: "Por favor, inténtelo de nuevo después de 5 segundos."

View File

@@ -308,6 +308,8 @@ fr:
sku: "Référence Produit"
subtotal: "Sous-total"
tax_rate: "TVA applicable"
with_tax_incl: "%{amount} taxes inclus"
producer_mail_qty: Quantité
validators:
date_time_string_validator:
not_string_error: "doit être une série"
@@ -527,6 +529,15 @@ fr:
depth: "Profondeur"
payment_could_not_process: "Le paiement n'a pas pu être traité"
payment_could_not_complete: "Le paiement n'a pas pu être finalisé"
vine_voucher_validator_service:
errors:
vine_api: "Il y a eu une erreur de communication avec l'API, merci de réessayer."
invalid_voucher: "Le bon de réduction n'est pas valide."
not_found_voucher: "Désolé, nous n'avons pas trouvé ce bon de réduction. Merci de vérifier le code qui vous a été transmis."
vine_voucher_redeemer_service:
errors:
vine_api: "Il y a eu une erreur de communication avec l'API"
redeeming_failed: "Echec de la prise en compte du bon de réduction"
actions:
create_and_add_another: "Créer et ajouter nouveau"
create: "Créer"
@@ -718,7 +729,7 @@ fr:
enabled_legend: "Applications connectées autorisées"
connected_apps_enabled:
discover_regen: Portail Discover Regenerative
affiliate_sales_data: API de commandes anonymisées DFC à fins de recherche
affiliate_sales_data: API DFC de commandes anonymisées à fins de recherche
vine: Éditeur de bons de réduction (VINE)
update:
resource: Paramètres de l'application connectée
@@ -901,6 +912,7 @@ fr:
category_field_name: "Catégorie"
tax_category_field_name: "TVA applicable"
producer_field_name: "Producteur"
select_unit_scale: Sélectionner l'échelle d'unité
clone:
success: Le produit a bien été dupliqué
error: Impossible de dupliquer le produit
@@ -1343,7 +1355,7 @@ fr:
connected_apps:
legend: "Applications connectées"
affiliate_sales_data:
title: "Programme de recherche INRAE / UFC QUE CHOISIR"
title: "Programme de recherche INRAE"
tagline: "Autoriser ce programme de recherche à accéder à vos données de commandes anonymisées"
enable: "Autoriser le partage de données"
disable: "Arrêter le partage"
@@ -1351,7 +1363,7 @@ fr:
need_to_be_manager: "Seuls les gestionnaires peuvent connecter des applications."
description_html: |
<p>
L'INRAE et UFC QUE CHOISIR travaillent conjointement sur les prix des produits distribués en circuit court, comparés aux prix des mêmes produits vendus en supermarché, pour un échantillon de produits. Les données utilisées dans le cadre de ce programme de recherche sont une agrégation des données fournies par plusieurs plateformes de vente en circuit court en France. Aucun prix de produit d'une boutique en particulier ne sera transmis publiquement à travers ce programme.
L'INRAE travaille sur les prix des produits distribués en circuit court, comparés aux prix des mêmes produits vendus en supermarché, pour un échantillon de produits. Les données utilisées dans le cadre de ce programme de recherche sont une agrégation des données fournies par plusieurs plateformes de vente en circuit court en France. Aucun prix de produit d'une boutique en particulier ne sera transmis publiquement à travers ce programme.
</p>
<p>
<a href="https://apropos.coopcircuits.fr/"
@@ -2038,6 +2050,8 @@ fr:
no_shipping_methods_available: Il n'est pas possible de passer commande car il n'y a pas d'options de livraison / récupération de commande. Veuillez contacter le gestionnaire de la boutique.
voucher_not_found: Non trouvé
add_voucher_error: Il y a eu une erreur lors de l'ajout de votre réduction.
create_voucher_error: "Il y a eu une erreur lors de la création du bon de réduction : %{error}"
voucher_redeeming_error: Il y a eu une erreur lors de la tentative d'utilisation de votre bon de réduction.
shops:
hubs:
show_closed_shops: "Afficher toutes les boutiques"
@@ -2651,6 +2665,7 @@ fr:
confirm_hub_change: "Confirmer? Cette action modifiera la boutique sélectionnée et tous les articles de votre panier seront effacés."
confirm_oc_change: "Confirmer? Cette action modifiera le cycle de vente sélectionné et tous les articles de votre panier seront effacés."
location_placeholder: "Saisissez une localisation..."
gmap_load_failure: "Impossible de charger la carte. Veuillez vérifier les paramètres de votre navigateur et accepter les cookies pour ce site web."
error_required: "Champ obligatoire"
error_number: "saisir un nombre"
error_email: "saisir une adresse email"
@@ -3192,11 +3207,11 @@ fr:
report_header_total_units: Vol. total
report_header_sum_max_total: "Somme Max Total"
report_header_total_excl_vat: "Total HT (%{currency_symbol})"
report_header_total_fees_excl_tax: "Total commission boutique hors taxe (%{currency_symbol})"
report_header_total_tax_on_fees: "Total taxe sur la commission boutique (%{currency_symbol})"
report_header_total_fees_excl_tax: "Total commission fournisseur hors taxe (%{currency_symbol})"
report_header_total_tax_on_fees: "Total taxe sur la commission fournisseur (%{currency_symbol})"
report_header_total: "Total (%{currency_symbol})"
report_header_total_incl_vat: "Total TTC (%{currency_symbol})"
report_header_total_excl_fees_and_tax: "Total hors commission boutique et taxe (%{currency_symbol})"
report_header_total_excl_fees_and_tax: "Total hors commission fournisseur et taxe (%{currency_symbol})"
report_header_temp_controlled: Temp Contrôlée ?
report_header_is_producer: Producteur ?
report_header_not_confirmed: Non confirmé

View File

@@ -107,6 +107,9 @@ fr_CA:
count_on_hand:
using_producer_stock_settings_but_count_on_hand_set: "doit être vide car utilise les informations de stock du producteur"
limited_stock_but_no_count_on_hand: "doit être spécifié car pas \"à volonté\""
connected_apps:
vine:
api_request_error: "Une erreur s'est produite lors de la connexion à l'API VINE"
messages:
confirmation: "ne correspond pas %{attribute}"
blank: "Champ obligatoire"
@@ -525,6 +528,10 @@ fr_CA:
depth: "Profondeur"
payment_could_not_process: "Le paiement n'a pas pu être traité"
payment_could_not_complete: "Le paiement n'a pas pu être finalisé"
vine_voucher_validator_service:
errors:
vine_api: "There was an error communicating with the API, please try again later."
invalid_voucher: "The voucher is not valid"
actions:
create_and_add_another: "Créer et ajouter nouveau"
create: "Créer"
@@ -717,6 +724,7 @@ fr_CA:
connected_apps_enabled:
discover_regen: Portail Discover Regenerative
affiliate_sales_data: API de commandes anonymisées DFC à fins de recherche
vine: Éditeur de bons de réduction (VINE)
update:
resource: Paramètres de l'application connectée
customers:
@@ -898,6 +906,7 @@ fr_CA:
category_field_name: "Catégorie"
tax_category_field_name: "Type de taxe"
producer_field_name: "Producteur"
select_unit_scale: Select unit scale
clone:
success: Le produit a bien été dupliqué
error: Impossible de dupliquer le produit
@@ -1376,9 +1385,25 @@ fr_CA:
<i class="icon-external-link"></i></a>
</p>
vine:
title: "Éditeur de bons de réduction (VINE)"
tagline: "Autoriser l'utilisation de bons de réduction VINE dans votre boutique"
enable: "Se connecter"
disable: "Déconnecter"
need_to_be_manager: "Seuls les gestionnaires peuvent connecter des applications."
vine_api_key: "clé API VINE"
vine_secret: "VINE secret"
description_html: |
<p>
Pour mettre en place l'éditeur de bons de réduction VINE pour votre entreprise, entrer votre clé API et secret.
</p>
<p>
<a href="#" target="_blank"><b>VINE</b>
<i class="icon-external-link"></i></a>
</p>
api_parameters_empty: "Entrer une clé API et un secret"
api_parameters_error: "Vérifiez que vous avez entré la clé API et le secret correctement. Contactez l'équipe support de la plateforme si l'erreur persiste. "
connection_error: "Erreur de connexion à l'API. Merci de réessayer."
setup_error: "L'API VINE n'est pas configurée. Veuillez contacter l'équipe support de la plateforme."
actions:
edit_profile: Paramètres
properties: Labels / propriétés
@@ -1675,6 +1700,7 @@ fr_CA:
pack_by_customer: Préparation des commandes par Acheteur
pack_by_supplier: Préparation des commandes par Producteur
pack_by_product: Préparation des commandes par Produit
pay_your_suppliers: Payer vos fournisseurs
display:
report_is_big: "Ce rapport est volumineux et risque de ralentir l'appareil sur lequel vous êtes en train de le consulter."
display_anyway: "Afficher quand même"
@@ -1722,6 +1748,8 @@ fr_CA:
enterprise_fee_summary:
name: "Résumé des marges et commissions"
description: "Résumé des marges et commissions collectées"
suppliers:
name: Fournisseurs
enterprise_fees_with_tax_report_by_order: "Détail des montants de taxe par commande"
enterprise_fees_with_tax_report_by_producer: "Détail des montants de taxe par producteur"
errors:
@@ -2625,6 +2653,7 @@ fr_CA:
confirm_hub_change: "Confirmer? Cette action modifiera la boutique sélectionnée et tous les articles de votre panier seront effacés."
confirm_oc_change: "Confirmer? Cette action modifiera le cycle de vente sélectionné et tous les articles de votre panier seront effacés."
location_placeholder: "Saisissez une localisation..."
gmap_load_failure: "Unable to load map. Please check your browser settings and allow 3rd party cookies for this website."
error_required: "Champ obligatoire"
error_number: "saisir un nombre"
error_email: "saisir une adresse email"
@@ -3009,6 +3038,8 @@ fr_CA:
report_render_options: Mise en forme
report_header_ofn_uid: ID OFN
report_header_order_cycle: Cycle de Vente
report_header_order_cycle_start_date: Date d'ouverture du cycle de vente
report_header_order_cycle_end_date: Date de fermeture du cycle de vente
report_header_user: Utilisateur
report_header_email: Email
report_header_status: Statut
@@ -3029,6 +3060,7 @@ fr_CA:
report_header_hub_legal_name: "Raison sociale"
report_header_hub_contact_name: "Nom du contact"
report_header_hub_email: "Email public"
report_header_hub_contact_email: e-mail de contact de la boutique multi-producteurs
report_header_hub_owner_email: Email gestionnaire principal
report_header_hub_phone: "Numéro de téléphone"
report_header_hub_address_line1: "Adresse ligne 1"
@@ -3101,6 +3133,8 @@ fr_CA:
report_header_producer_suburb: Ville Producteur
report_header_producer_tax_status: Soumis à la taxe
report_header_producer_charges_sales_tax?: Soumis à la GST
report_header_producer_abn_acn: Numéro de SIRET/SIREN du producteur
report_header_producer_address: Adresse du producteur
report_header_unit: Unité
report_header_group_buy_unit_quantity: Nb d'unités achetées (vente par lots)
report_header_cost: Coût
@@ -3161,7 +3195,11 @@ fr_CA:
report_header_total_units: Vol. total
report_header_sum_max_total: "Somme Max Total"
report_header_total_excl_vat: "Total HT (%{currency_symbol})"
report_header_total_fees_excl_tax: "Total commission boutique hors taxe (%{currency_symbol})"
report_header_total_tax_on_fees: "Total taxe sur la commission boutique (%{currency_symbol})"
report_header_total: "Total (%{currency_symbol})"
report_header_total_incl_vat: "Total TTC (%{currency_symbol})"
report_header_total_excl_fees_and_tax: "Total hors commission boutique et taxe (%{currency_symbol})"
report_header_temp_controlled: Temp Contrôlée ?
report_header_is_producer: Producteur ?
report_header_not_confirmed: Non confirmé
@@ -3739,7 +3777,7 @@ fr_CA:
many: "pièces"
other: "pièces"
pot:
one: "pots"
one: "pot"
many: "pots"
other: "pots"
bundle:

View File

@@ -1,10 +1,15 @@
fr_CH:
language_name: "Français"
time:
formats:
long: "%e %b %Y, %-k:%M"
activerecord:
models:
spree/product: Produit
spree/shipping_method: Option d'expédition
attributes:
spree/image:
attachment: Pièce jointe
spree/order/ship_address:
phone: "Numéro de téléphone"
firstname: "Prénom"
@@ -551,6 +556,7 @@ fr_CH:
terms_of_service: "Conditions Générales d'Utilisation"
delete: "Supprimer le fichier"
confirm_delete: "Etes-vous certain de vouloir supprimer les CGU & CGV actuelles ?"
attachment: "Pièce jointe"
number_localization:
number_localization_settings: "Gestion localisation des nombres"
enable_localized_number: "Utiliser le traitement international des séparateurs de milliers/centimes"

View File

@@ -53,6 +53,7 @@ hu:
primary_taxon: "Termékkategória"
shipping_category_id: "Szállítási mód"
supplier: "Beszállító"
variant_unit: "Mértékegység"
variant_unit_name: "Változat egység neve"
unit_value: "Egység értéke"
spree/credit_card:
@@ -297,7 +298,7 @@ hu:
producer_signup_case_studies_html: "Termelői regisztrációs esettanulmányok HTML"
producer_signup_detail_html: "Termelői regisztráció részletek HTML"
producer_signup_pricing_table_html: "Termelői regisztrációs ártáblázat HTML"
producers_social: "Termelői szövetkezet"
producers_social: "Közösségi média"
resume_order: "Rendelés folytatása"
sku: "SKU"
subtotal: "Részösszeg"
@@ -534,7 +535,7 @@ hu:
producer: Termelő
category: Kategória
sku: SKU kód
on_hand: "Raktáron"
on_hand: "Készlet"
on_demand: "Igény szerint"
tax_category: "Adókategória"
inherits_properties: "Megörökli az tulajdonságokat?"
@@ -546,7 +547,7 @@ hu:
producer: Termelő
category: Kategória
sku: SKU kód
on_hand: "Raktáron"
on_hand: "Készlet"
on_demand: "Igény szerint"
tax_category: "Adókategória"
inherits_properties: "Megörökli az tulajdonságokat?"
@@ -680,7 +681,7 @@ hu:
matomo_tag_manager_url: "Matomo Címkekezelő URL-je"
info_html: "A Matomo egy webes és mobilelemző alkalmazás. A Matomo helyszíni üzemeltetését vagy felhőalapú szolgáltatást is használhatja. További információért lásd: <a href='http://matomo.org' target='_blank'>matomo.org</a>."
config_instructions_html: "Itt konfigurálhatja az OFN Matomo integrációt. Az alábbi Matomo URL-nek arra a Matomo-példányra kell mutatnia, ahová a felhasználó követési információkat küldi; ha üresen marad, a Matomo felhasználókövetés le lesz tiltva. A Webhelyazonosító mező nem kötelező, de hasznos, ha egynél több webhelyet követ nyomon egyetlen Matomo-példányon; a Matomo példánykonzolon található."
config_instructions_tag_manager_html: "A Matomo Címkekezelő URL-címének beállítása engedélyezi a Matomo Címkekezelőt. Ez az eszköz lehetővé teszi az elemzési események beállítását. A Matomo Címkekezelő URL-címe a Matomo Tag Manager Telepítési kód szakaszából lett kimásolva. Győződjön meg róla, hogy a megfelelő tárolót és környezetet választotta, mivel ezek a beállítások megváltoztatják az URL-t."
config_instructions_tag_manager_html: "A Matomo Címkekezelő URL-címének beállítása engedélyezi a Matomo Címkekezelőt. Ez az eszköz lehetővé teszi az elemzési események beállítását. A Matomo Címkekezelő URL-címe a Matomo Tag Manager Telepítési kód szakaszából lett kimásolva. Győződj meg róla, hogy a megfelelő tárolót és környezetet választotta, mivel ezek a beállítások megváltoztatják az URL-t."
connected_app_settings:
edit:
title: "Kapcsolt app beállítás"
@@ -860,6 +861,7 @@ hu:
category_field_name: "Kategória"
tax_category_field_name: "Adókategória"
producer_field_name: "Termelő"
select_unit_scale: Válassz mértékegységet
clone:
success: Sikeresen klónozta a terméket
error: A terméket nem sikerült klónozni
@@ -1061,9 +1063,9 @@ hu:
business_details:
legend: "Vállalkozás adatai"
upload: 'feltöltés'
abn: LÉGI ÚTON SZÁLLÍTOTT
abn: ADÓSZÁM
abn_placeholder: pl. 99 123 456 789
acn: Közösségi Adószám (EU)
acn: Cégjegyzékszám
acn_placeholder: pl. 123 456 789
display_invoice_logo: Logó megjelenítése a vevői összesítőkön
invoice_text: Adj testreszabott szöveget a vevői összesítők végéhez
@@ -1197,7 +1199,7 @@ hu:
enable_subscriptions_false: "Tiltva"
enable_subscriptions_true: "Engedélyezve"
customer_names_in_reports: "Ügyfélnevek a jelentésekben"
customer_names_tip: "Engedélyezze beszállítóinak, hogy lássák az ügyfelek nevét a jelentésekben"
customer_names_tip: "Engedélyezd a beszállítóidnak, hogy lássák az ügyfelek nevét a jelentésekben"
customer_names_false: "Tiltva"
customer_names_true: "Engedélyezve"
shopfront_message: "Online Kirakat üzenete"
@@ -1275,7 +1277,7 @@ hu:
vouchers:
legend: Kuponok
voucher_code: Kupon kód
rate: Mérték
rate: Arány
label: Címke
purpose: Cél
expiry: Lejárati idő
@@ -1317,7 +1319,7 @@ hu:
</p>
discover_regen:
title: "Discover Regenerative"
tagline: "Engedélyezd a Discover Regenerative számára a vállalati adatok közzétételét."
tagline: "Engedélyezd a Discover Regenerative számára a vállalkozási adatok közzétételét."
enable: "Adatmegosztás engedélyezése"
disable: "Megosztás leállítása"
loading: "Betöltés"
@@ -1338,6 +1340,7 @@ hu:
enable: "Csatlakozás"
disable: "Lecsatlakozás"
need_to_be_manager: "Csak a megfelelő jogosultsággal rendelkező felhasználók csatlakoztathatnak alkalmazásokat."
vine_api_key: "VINE API kulcs"
connection_error: "API kapcsolódási hiba, kérjük próbáld újra"
actions:
edit_profile: Beállítások
@@ -1391,8 +1394,10 @@ hu:
contact_name: Kapcsolattartó neve
edit:
editing: 'Beállítások:'
back_link: Vissza a válllalkozások listájához
new:
title: Új Vállalkozás
back_link: Vissza a válllalkozások listájához
welcome:
welcome_title: Üdvözölünk az Open Food Network-ben!
welcome_text: Sikeresen létrehoztad a
@@ -1633,6 +1638,7 @@ hu:
pack_by_customer: Összekészítés Ügyfelenként
pack_by_supplier: Összekészítés Beszállítónként
pack_by_product: Összekészítés Termékenként
pay_your_suppliers: Fizetés a beszállítóknak
display:
report_is_big: "Ez a jelentés nagy méretű, így lelassíthatja az eszközödet."
display_anyway: "Megjelenítés mindenképp"
@@ -1678,6 +1684,8 @@ hu:
enterprise_fee_summary:
name: "Vállalkozási díjak összefoglalója"
description: "Összefoglaló a beszedett vállalkozási díjakról"
suppliers:
name: Beszállítók
enterprise_fees_with_tax_report_by_order: "Vállalkozási díjak adójelentéssei megrendelés alapján"
enterprise_fees_with_tax_report_by_producer: "Vállalkozási díjak Jelentés termelőnként"
errors:
@@ -1688,7 +1696,7 @@ hu:
summary_row:
total: "ÖSSZESEN"
table:
select_and_search: "Válaszd ki a szűrőket, és kattintson a %{option} elemre az adatok eléréséhez."
select_and_search: "Válaszd ki a szűrőket, és kattints a %{option} elemre az adatok eléréséhez."
headings:
hub: "Átvételi pont"
customer_code: "Kód"
@@ -1745,18 +1753,18 @@ hu:
setup_explanation:
title: "Előfizetések"
just_a_few_more_steps: 'Még néhány lépés, mielőtt elkezdhetné:'''
enable_subscriptions: "Engedélyezze a feliratkozásokat legalább egy oldalán"
enable_subscriptions_step_1_html: 1. Nyissa meg a %{enterprises_link} oldalt, keresse meg az oldalát, és kattintson a "Kezelés" gombra
enable_subscriptions_step_2: 2. Az "Oldalbeállítások" alatt", engedélyezze az Előfizetések opciót
set_up_shipping_and_payment_methods_html: Állítson be %{shipping_link} és %{payment_link} metódust
set_up_shipping_and_payment_methods_note_html: Ne feledje, hogy előfizetésekhez csak készpénzes és Stripe fizetési módok<br />használhatók
ensure_at_least_one_customer_html: Győződjön meg arról, hogy legalább egy %{customer_link} létezik
enable_subscriptions: "Engedélyezd a feliratkozásokat legalább egy oldalon"
enable_subscriptions_step_1_html: 1. Nyisd meg a %{enterprises_link} oldalt, keresd meg az oldalad, és kattints a "Kezelés" gombra
enable_subscriptions_step_2: 2. Az "Oldalbeállítások" alatt", engedélyezd az Előfizetések opciót
set_up_shipping_and_payment_methods_html: Állíts be %{shipping_link} és %{payment_link} metódust
set_up_shipping_and_payment_methods_note_html: Ne feledd, hogy előfizetésekhez csak készpénzes és Stripe fizetési módok<br />használhatók
ensure_at_least_one_customer_html: Győződj meg arról, hogy legalább egy %{customer_link} létezik
create_at_least_one_schedule: Hozz létre legalább egy ütemezést
create_at_least_one_schedule_step_1_html: '1. Nyissa meg a következőt: %{order_cycles_link}'
create_at_least_one_schedule_step_2: 2. Hozz létre egy rendelési ciklust, ha még nem tette meg
create_at_least_one_schedule_step_3: 3. Kattintson a "+ Új ütemezés" gombra.", és töltse ki az űrlapot
create_at_least_one_schedule_step_1_html: '1. Nyisd meg a következőt: %{order_cycles_link}'
create_at_least_one_schedule_step_2: 2. Hozz létre egy rendelési ciklust, ha még nem tetted meg
create_at_least_one_schedule_step_3: 3. Kattints a "+ Új ütemterv" gombra.", és töltsd ki az űrlapot
once_you_are_done_you_can_html: Ha végeztél, %{reload_this_page_link}
reload_this_page: töltse be újra ezt az oldalt
reload_this_page: töltsd be újra ezt az oldalt
form:
create: "Előfizetés létrehozása"
steps:
@@ -1775,7 +1783,7 @@ hu:
add: "Hozzáadás"
details:
details: Részletek
invalid_error: Hoppá! Kérjük, töltse ki az összes kötelező mezőt...
invalid_error: Hoppá! Kérjük, töltsd ki az összes kötelező mezőt...
allowed_payment_method_types_tip: Jelenleg csak készpénzes és Stripe fizetési mód használhatók
credit_card: Hitelkártya
charges_not_allowed: Ez az ügyfél nem engedélyezi a díjakat
@@ -1843,7 +1851,7 @@ hu:
other: "%{count} megrendelés esetén visszaigazoló e-mailt küldtünk."
send_invoice_feedback:
one: "1 rendelés számláját elküldtük emailben."
other: "%{count} rendelés számláját elküldtük emailben."
other: "%{count} rendelés összegzését elküldtük emailben."
api:
unknown_error: "Valami elromlott. Csapatunk értesítést kapott."
invalid_api_key: "Érvénytelen API-kulcs (%{key}) van megadva."
@@ -2050,7 +2058,7 @@ hu:
total_incl_tax: "Összesen (adóval együtt):"
total_all_tax: "Összes adó:"
abn: "Adószám:"
acn: "Közösségi Adószám (EU):"
acn: "Cégjegyzékszám:"
invoice_issued_on: "Kiállítás dátuma:"
order_number: "Rendelés sorszáma:"
date_of_transaction: "A tranzakció dátuma:"
@@ -2195,7 +2203,7 @@ hu:
cookies_policy_link_desc: "Ha többet szeretne megtudni, nézze meg oldalunkat"
cookies_policy_link: "cookie-kra vonatkozó szabályzat"
cookies_accept_button: "Cookie-k elfogadása"
home_shop: Tovább a termelőkhöz, átvételi pontokhoz.
home_shop: Tovább a termelőkhöz, átvételi pontokhoz
brandstory_headline: "Együtt, az élelem önrendelkezésért!"
brandstory_intro: "Közösség által irányított élelmiszerrendszert építünk."
brandstory_part1: "Az alapoktól kezdjük. \nA gazdálkodókkal, akiknek fontos, hogy közösen, egy fenntartható és mindenki számára átlátható, hazai élelemrendszert hozzunk létre. \nA bevásárlóközösségekkel, fogyasztói csoportokkal, más elosztókkal, akik összekapcsolják az embereket a termelőkkel és a termékekkel, és biztosítani szeretnék, hogy a termelők tisztességes díjazásban részesüljenek. \nAzokkal a fogyasztókkal, akik hisznek abban, hogy döntéseiknek hatása van."
@@ -2212,7 +2220,7 @@ hu:
system_step3: "3. Átvétel / kiszállítás"
system_step3_text: "Válaszd a kiszállítást, vagy a személyesebb kapcsolat érdekében keresd fel a termelőt, vagy az átvételi pontot."
cta_headline: "A jövőnk fenntartható élelmiszerhálózata."
cta_label: "Tovább a termelőkhöz, átvételi pontokhoz."
cta_label: "Tovább a termelőkhöz, átvételi pontokhoz"
stats_headline: "Termelők és fogyasztók közösségét építjük."
stats_producers: "élelmiszer-termelők"
stats_shops: "átvételi pontok"
@@ -2274,7 +2282,7 @@ hu:
products_in: "itt: %{oc}"
products_at: "itt: %{distributor}"
products_elsewhere: "Máshol talált termékek"
email_confirmed: "Köszönjük, hogy megerősítette e-mail címét."
email_confirmed: "Köszönjük, hogy megerősítetted az e-mail címed."
email_confirmation_activate_account: "Mielőtt aktiválhatnánk az új fiókodat, meg kell erősíteni az e-mail címed."
email_confirmation_greeting: "Kedves %{contact}!"
email_confirmation_profile_created: "A %{name} profilod sikeresen létrejött! Profilod aktiválásához meg kell erősíteni ezt az e-mail címet."
@@ -2341,7 +2349,7 @@ hu:
greeting: "Üdv!"
invited_to_manage: "Meghívást kaptál, hogy kezeld a(z) %{enterprise} %{instance} szolgáltatást."
confirm_your_email: "Egy megerősítő linket tartalmazó e-mailt kellett volna kapnia, vagy hamarosan kapni fog. Addig nem férhetsz hozzá %{enterprise} profiljához, amíg meg nem erősíted az e-mail címedet."
set_a_password: "Ezután a rendszer kéri, hogy állítson be egy jelszót, mielőtt felügyelheti a vállalkozásot."
set_a_password: "Ezután a rendszer kéri, hogy állítson be egy jelszót, mielőtt felügyelheti a vállalkozást."
mistakenly_sent: "Nem tudod, miért kaptad ezt az e-mailt? További információért fordulj %{owner_email}-hoz."
producer_mail_greeting: "Kedves"
producer_mail_text_before: "Alább megtalálja a rendelési ciklusra vonatkozó frissítést:"
@@ -2372,7 +2380,7 @@ hu:
maps_open: "Nyitva"
maps_closed: "Zárva"
map_title: "Térkép"
hubs_buy: "Kínálat:"
hubs_buy: "Termékkínálat:"
hubs_shopping_here: "Vásárlás itt"
hubs_orders_closed: "A rendelések lezárva"
hubs_profile_only: "Csak profil"
@@ -2419,7 +2427,7 @@ hu:
products_changes_saved: "Változtatások elmentve."
products_no_results_html: "Sajnáljuk, nem található találat a következőre: %{query}"
products_clear_search: "Keresés törlése"
search_no_results_html: "Sajnáljuk, nem találtunk Átvételi pontot/Termelőt a következő szűrésre: %{query} Próbálkozz másik kereséssel!"
search_no_results_html: "Sajnáljuk, nem találtunk Átvételi pontot/Termelőt a következő szűrésre: %{query} \nPróbálkozz másik kereséssel!"
components_profiles_popover: "A Profiloknak nincs online kirakata az Open Food Network-ben, de lehet, hogy van saját fizikai vagy online átvételi pontjuk máshol."
components_profiles_show: "Profilok megjelenítése"
components_filters_nofilters: "Nincsenek szűrők"
@@ -2458,7 +2466,7 @@ hu:
groups_signup_detail: "Itt a részlet."
login_invalid: "Rossz email cím vagy jelszó"
producers_about: Rólunk
producers_buy: Vásárolni valakinek
producers_buy: 'Kínálat:'
producers_contact: Kapcsolat
producers_contact_phone: Hívás
producers_contact_social: Kövesse
@@ -2494,7 +2502,7 @@ hu:
sell_hubs: "Átvételi Pontok"
sell_groups: "Csoportok"
sell_producers_detail: "Őstermelő, kistermelő, családi gazdálkodó, vagy épp kisválllalkozás vagy? Hozz létre termelői profilt az OFN-en percek alatt. Bármikor frissítheted profilod egy online áruházra, és közvetlenül ajánlhatod termékeidet a fogyasztóknak."
sell_hubs_detail: "Bevásárlóközösséget indítanál? Vagy csak összefognál a termelőtársakkal és közösen szólítanátok meg a fogyasztókat? Hozz létre új profilt, vagy bármikor frissítheted a profilodat egy többtermelős átvételi pontra."
sell_hubs_detail: "Bevásárlóközösséget indítanál, vagy termelői piacot működtetsz? Vagy csak összefognál a termelőtársakkal és közösen szólítanátok meg a fogyasztókat? Hozz létre új profilt, vagy bármikor frissítheted a profilodat egy többtermelős átvételi pontra."
sell_groups_detail: "Hozz létre egy személyre szabott jegyzéket (termelők és egyéb élelmiszeripari vállalkozások) régiója vagy szervezete számára."
sell_user_guide: "Tudj meg többet használati útmutatónkból."
sell_listing_price: "A termelői regisztráció az OFN-en ingyenes, és az is marad. A platformon történő értékesítés volumenétől függően azonban közösségi hozzájárulást kérünk, a szoftver üzemeltetéshez és IT support biztosításához . \nRészletek hamarosan. "
@@ -2690,7 +2698,7 @@ hu:
enterprise_long_desc_length: "%{num} karakter / legfeljebb 600 ajánlott"
enterprise_abn: "Adószám"
enterprise_abn_placeholder: "például. 99 123 456 789"
enterprise_acn: "Közösségi Adószám (EU)"
enterprise_acn: "Cégjegyzékszám"
enterprise_acn_placeholder: "például. 123 456 789"
enterprise_tax_required: "Ki kell választani."
images:
@@ -2790,7 +2798,7 @@ hu:
admin_enterprise_relationships_permits: "engedélyezi"
admin_enterprise_relationships_seach_placeholder: "Keresés"
admin_enterprise_relationships_button_create: "Létrehozás"
admin_enterprise_relationships_to: "kapcsolat vele"
admin_enterprise_relationships_to: "számára"
admin_enterprise_groups: "Vállalkozási csoportok"
admin_enterprise_groups_name: "Név"
admin_enterprise_groups_owner: "Tulajdonos"
@@ -2964,6 +2972,8 @@ hu:
report_render_options: Rendezési beállítások
report_header_ofn_uid: OFN Azonosító
report_header_order_cycle: Rendelési Ciklus
report_header_order_cycle_start_date: RC kezdő dátum
report_header_order_cycle_end_date: RC záró dátum
report_header_user: Felhasználó
report_header_email: Email
report_header_status: Státusz
@@ -2984,6 +2994,7 @@ hu:
report_header_hub_legal_name: "Átvételi Pont hivatalos neve"
report_header_hub_contact_name: "Átvételi Pont kapcsolattartó neve"
report_header_hub_email: "Átvételi Pont nyilvános email"
report_header_hub_contact_email: Átvételi pont kapcsolattartói email
report_header_hub_owner_email: Átvételi Pont üzemeltető email
report_header_hub_phone: "Átvételi Pont telefonszám"
report_header_hub_address_line1: "Átvételi Pont cím 1"
@@ -3044,18 +3055,20 @@ hu:
report_header_quantity: Mennyiség
report_header_max_quantity: Max Mennyiség
report_header_variant: Változat
report_header_variant_unit_name: Változat egység neve
report_header_variant_unit_name: Változat egysége
report_header_variant_value: Változat értéke
report_header_variant_unit: Változat egysége
report_header_total_available: Összesen elérhető
report_header_unallocated: Kiosztatlan
report_header_max_quantity_excess: Maximális Túllépési Mennyiség
report_header_taxons: Taxonok
report_header_taxons: Termékkategóriák
report_header_supplier: Beszállító
report_header_producer: Termelő
report_header_producer_suburb: Termelő kerület
report_header_producer_tax_status: Termelő adókategóriája
report_header_producer_charges_sales_tax?: ÁFA nyilvántartott
report_header_producer_abn_acn: Termelő adószáma/cégjegyzékszáma
report_header_producer_address: Termelő címe
report_header_unit: Mértékegység
report_header_group_buy_unit_quantity: Csoportos vásárlási egység mennyisége
report_header_cost: Költség
@@ -3115,8 +3128,12 @@ hu:
report_header_total_max: Összesen max
report_header_total_units: Összes egység
report_header_sum_max_total: "Összeg Max összesen"
report_header_total_excl_vat: "Összesen, kivéve adó (%{currency_symbol})"
report_header_total_excl_vat: "Összesen, adó nélkül (%{currency_symbol})"
report_header_total_fees_excl_tax: "Össz díjak adó nélkül (%{currency_symbol})"
report_header_total_tax_on_fees: "Össz adó a díjakon (%{currency_symbol})"
report_header_total: "Összesen (%{currency_symbol})"
report_header_total_incl_vat: "Összesen, beleértve adó (%{currency_symbol})"
report_header_total_excl_fees_and_tax: "Összesen díjak és adók nélkül (%{currency_symbol})"
report_header_temp_controlled: Hűtést igényel?
report_header_is_producer: Termelő?
report_header_not_confirmed: Nem megerősített
@@ -3256,7 +3273,7 @@ hu:
enterprise_limit_reached: "Elérted a vállalkozások számlaszámonkénti standard korlátját. Írj a %{contact_email} címre, ha növelni szeretnéd."
deleting_item_will_cancel_order: "Ez a művelet egy vagy több üres rendelést eredményez, amelyek törlésre kerülnek. Szeretné folytatni?"
modals:
got_it: "Megvan"
got_it: "Értem"
confirm: "Megerősítés"
close: "Bezárás"
continue: "Tovább"
@@ -3830,7 +3847,7 @@ hu:
cannot_create_returns: "Nem tudsz visszárut létrehozni, mivel a rendelésnek nincsenek kiszállított egységei."
select_stock: "Válassz a készletről"
location: "Elhelyezkedés"
count_on_hand: "Count On Hand"
count_on_hand: "Készleten lévő mennyiség"
quantity: "Mennyiség"
on_demand: "Igény szerint"
on_hand: "Készlet"
@@ -4066,7 +4083,7 @@ hu:
product_properties:
index:
inherits_properties_checkbox_hint: "Tulajdonságokat örököl a következőtől: %{supplier}? (hacsak nincs felülírva)"
add_product_properties: "Add hozzá a termék tulajdonságait"
add_product_properties: "További termék tulajdonság hozzáadása"
properties:
index:
properties: "Tulajdonságok"
@@ -4482,7 +4499,7 @@ hu:
out_of_stock: "Nincs raktáron"
unavailable_item: "Jelenleg nem elérhető"
shipment_states:
backorder: utólagos rendelés
backorder: visszamaradt rendelés/nem rendelhető
partial: részleges
pending: függőben levő
ready: kész
@@ -4537,7 +4554,7 @@ hu:
subject: "Kérjük, erősítsd meg Open Food fiókod"
payment_mailer:
authorize_payment:
subject: "Kérjük, engedélyezze fizetését %{distributor} részére az Open Food-on"
subject: "Kérjük, engedélyezd fizetést %{distributor} részére az Open Food-on"
instructions: "%{amount} összegű befizetésed %{distributor} részére további hitelesítést igényel. Kérjük, keresd fel a következő URL-t a fizetés engedélyezéséhez:"
authorization_required:
subject: "A fizetéshez az ügyfél engedélye szükséges"

View File

@@ -0,0 +1,7 @@
class AddExternalVoucherIdExternalVoucherSetIdVoucherTypeToVouchers < ActiveRecord::Migration[7.0]
def change
add_column :vouchers, :external_voucher_id, :uuid
add_column :vouchers, :external_voucher_set_id, :uuid
add_column :vouchers, :voucher_type, :string
end
end

View File

@@ -0,0 +1,9 @@
class UpdateIndexAndRemoveVoucherTypeFromVoucher < ActiveRecord::Migration[7.0]
def change
remove_column :vouchers, :voucher_type
remove_index :vouchers, [:code, :enterprise_id], unique: true
add_index :vouchers, [:code, :enterprise_id]
add_index :vouchers, [:code, :enterprise_id, :external_voucher_id], name: "index_vouchers_on_code_and_enterprise_id_and_ext_voucher_id"
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_10_30_033956) do
ActiveRecord::Schema[7.0].define(version: 2024_11_12_230401) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "plpgsql"
@@ -1110,7 +1110,10 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_30_033956) do
t.datetime "deleted_at", precision: nil
t.decimal "amount", precision: 10, scale: 2, default: "0.0", null: false
t.string "type", limit: 255, default: "Vouchers::FlatRate", null: false
t.index ["code", "enterprise_id"], name: "index_vouchers_on_code_and_enterprise_id", unique: true
t.uuid "external_voucher_id"
t.uuid "external_voucher_set_id"
t.index ["code", "enterprise_id", "external_voucher_id"], name: "index_vouchers_on_code_and_enterprise_id_and_ext_voucher_id"
t.index ["code", "enterprise_id"], name: "index_vouchers_on_code_and_enterprise_id"
t.index ["deleted_at"], name: "index_vouchers_on_deleted_at"
t.index ["enterprise_id"], name: "index_vouchers_on_enterprise_id"
end

View File

@@ -21,7 +21,8 @@ Note: There is no need to install Docker Desktop on Linux.
* You may have to deselect the option to use Docker Compose V2 in Docker settings to make our scripts work.
## Getting Started
### Linux
1. **Clone the Repository**:
* Open a terminal with a shell.
* Clone the repository. If you're planning on contributing code to the project (which we [LOVE](CONTRIBUTING.md)), begin by forking this repo with the Fork button in the top-right corner of this screen.
* Use git clone to copy your fork onto your local machine.
@@ -32,48 +33,50 @@ $ git clone https://github.com/YOUR_GITHUB_USERNAME_HERE/openfoodnetwork
```sh
$ git clone git@github.com:openfoodfoundation/openfoodnetwork.git
```
* Go at the root of the app:
```sh
$ cd openfoodnetwork
```
* Download the Docker images, build the Docker containers, seed the database with sample data, AND log the screen output from these tasks:
```sh
$ docker/build
```
* Run the Rails server and its required Docker containers:
```sh
$ docker/server
```
* The default admin user is 'ofn@example.com' with the password 'ofn123'.
* View the app in the browser at `http://localhost:3000`.
* You will then get the trace of the containers in the terminal. You can stop the containers using Ctrl-C in the terminal.
* You can find some useful tips and commands [here](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Docker:-useful-tips-and-commands).
### Windows
* Prerequisite : don't forget to activate the execution of powershell scripts following the instruction on [this page chosing "Using RemoteSigned Execution Policy"](https://shellgeek.com/powershell-fix-running-scripts-is-disabled-on-this-system/)
* Open a terminal with a shell command.
* Clone the repository. If you're planning on contributing code to the project (which we [LOVE](CONTRIBUTING.md)), begin by forking this repo with the Fork button in the top-right corner of this screen.
* Use git clone to copy your fork onto your local machine.
```command
$ git clone https://github.com/YOUR_GITHUB_USERNAME_HERE/openfoodnetwork
```
* Otherwise, if you just want to get things running, clone from the OFN main repo:
```command
$ git clone git@github.com:openfoodfoundation/openfoodnetwork.git
```
* Go at the root of the app:
```command
2. **Navigate to the App Directory**:
```sh
$ cd openfoodnetwork
```
* Download the Docker images, build the Docker containers, seed the database with sample data, AND log the screen output from these tasks:
```command
$ docker/build.ps1
```
3. **Choose and Build Docker Image**:
* You have two choices of images you can build. The default Ubuntu image is most similar to the production servers but is larger in size and will take longer to build. Alternatively, you can use the Alpine image which is smaller, faster to setup, and should be compatible with the Ubuntu. However, it will need to be updated when ruby is updated. To download the Docker images, build the Docker containers, seed the database with sample data, AND log the screen output from these tasks:
- **Ubuntu Image** (larger, production-like):
- **Linux**:
```sh
$ docker/build
```
- **Windows**:
```command
$ docker/build.ps1
```
- **Alpine Image** (smaller, faster):
- **Linux**:
```sh
$ docker/build alpine.Dockerfile
```
- **Windows**:
```command
$ docker/build.ps1 alpine.Dockerfile
```
4. **Run the Rails Server**:
* Run the Rails server and its required Docker containers:
```command
$ docker/server.ps1
```
You may need to wait several minutes before getting the server up and running properly.
- **Linux**:
```sh
$ docker/server
```
- **Windows**:
```command
$ docker/server.ps1
```
* To run tests or access the server directly, you can use bash:
```sh
$ docker compose exec web bash
```
> **Note**: Windows users may need to enable PowerShell script execution by setting the RemoteSigned policy. See instructions [here](https://shellgeek.com/powershell-fix-running-scripts-is-disabled-on-this-system/).
* The default admin user is 'ofn@example.com' with the password 'ofn123'.
* View the app in the browser at `http://localhost:3000`.
* You will then get the trace of the containers in the terminal. You can stop the containers using Ctrl-C in the terminal.

View File

@@ -4,5 +4,5 @@ set -e
# This script builds the Docker container, seeds the app with sample data, and logs the screen output.
DATE=`date +%Y%m%d-%H%M%S-%3N`
docker/build-log 2>&1 | tee log/build-$DATE.log
docker/build-log $1 2>&1 | tee log/build-$DATE.log
docker/seed 2>&1 | tee log/seed-$DATE.log

View File

@@ -6,7 +6,11 @@ wait
echo '###########################'
echo 'BEGIN: docker compose build'
echo '###########################'
docker compose build # Set up the Docker containers
if [ -n "$1" ]; then
docker build -f $1 . # Set up the Docker containers
else
docker compose build # Set up the Docker containers
fi
echo '##############################'
echo 'FINISHED: docker compose build'
echo '##############################'

View File

@@ -1,5 +1,12 @@
Write-Host "Docker cleaning: remove old containers" -ForegroundColor Blue
docker compose down -v --remove-orphans
Write-Host "Docker build: set up the docker containers" -ForegroundColor Blue
docker compose build
# Check if an argument is provided
if ($args.Count -gt 0) {
# Run the build command with the argument and log output
docker build -f $1 .
} else {
docker compose build
}
Write-Host "Docker build finished"

View File

@@ -4,5 +4,11 @@
$DateTime=Get-Date -Format "yyyyMMdd-HHmmss"
docker/build-log.ps1 > log/build-$DateTime.log 2>&1
# Check if an argument is provided
if ($args.Count -gt 0) {
# Run the build command with the argument and log output
docker/build-log.ps1 $args[0] > log/build-$DateTime.log 2>&1
} else {
docker/build-log.ps1 > log/build-$DateTime.log 2>&1
}
docker/seed.ps1 > log/seed-$DateTime.log 2>&1

View File

@@ -12,7 +12,10 @@ class AffiliateSalesDataRowBuilder < DfcBuilder
def build_supplier
DataFoodConsortium::Connector::Enterprise.new(
nil,
localizations: [build_address(item[:supplier_postcode])],
localizations: [build_address(
item[:supplier_postcode],
item[:supplier_country]
)],
suppliedProducts: [build_product],
)
end
@@ -20,7 +23,10 @@ class AffiliateSalesDataRowBuilder < DfcBuilder
def build_distributor
DataFoodConsortium::Connector::Enterprise.new(
nil,
localizations: [build_address(item[:distributor_postcode])],
localizations: [build_address(
item[:distributor_postcode],
item[:distributor_country]
)],
)
end
@@ -89,9 +95,10 @@ class AffiliateSalesDataRowBuilder < DfcBuilder
)
end
def build_address(postcode)
def build_address(postcode, country)
DataFoodConsortium::Connector::Address.new(
nil,
country:,
postalCode: postcode,
)
end

View File

@@ -38,9 +38,11 @@ class AffiliateSalesQuery
JOIN spree_products ON spree_products.id = spree_variants.product_id
JOIN enterprises AS suppliers ON suppliers.id = spree_variants.supplier_id
JOIN spree_addresses AS supplier_addresses ON supplier_addresses.id = suppliers.address_id
JOIN spree_countries AS supplier_countries ON supplier_countries.id = supplier_addresses.country_id
JOIN spree_orders ON spree_orders.id = spree_line_items.order_id
JOIN enterprises AS distributors ON distributors.id = spree_orders.distributor_id
JOIN spree_addresses AS distributor_addresses ON distributor_addresses.id = distributors.address_id
JOIN spree_countries AS distributor_countries ON distributor_countries.id = distributor_addresses.country_id
SQL
end
@@ -53,7 +55,9 @@ class AffiliateSalesQuery
spree_variants.unit_presentation,
spree_line_items.price,
distributor_addresses.zipcode AS distributor_postcode,
distributor_countries.name AS distributor_country,
supplier_addresses.zipcode AS supplier_postcode,
supplier_countries.name AS supplier_country,
SUM(spree_line_items.quantity) AS quantity_sold
SQL
@@ -68,7 +72,9 @@ class AffiliateSalesQuery
spree_variants.unit_presentation,
spree_line_items.price,
distributor_postcode,
supplier_postcode
supplier_postcode,
distributor_country,
supplier_country
SQL
end
@@ -82,7 +88,9 @@ class AffiliateSalesQuery
unit_presentation
price
distributor_postcode
distributor_country
supplier_postcode
supplier_country
quantity_sold
]
end

View File

@@ -2,15 +2,31 @@
class DfcLoader
def self.connector
@connector ||= load_vocabularies
unless @connector
@connector = DataFoodConsortium::Connector::Connector.instance
load_context
load_vocabularies
end
@connector
end
def self.load_context
JSON::LD::Context.add_preloaded("http://www.datafoodconsortium.org/") {
JSON::LD::Context.parse(read_file("context_1.8.2")["@context"])
}
end
def self.vocabulary(name)
@vocabs ||= {}
@vocabs[name] ||= connector.__send__(:loadThesaurus, read_file(name))
end
def self.load_vocabularies
connector = DataFoodConsortium::Connector::Connector.instance
connector.loadMeasures(read_file("measures"))
connector.loadFacets(read_file("facets"))
connector.loadProductTypes(read_file("productTypes"))
connector
vocabulary("vocabulary") # order states etc
end
def self.read_file(name)

View File

@@ -9,3 +9,4 @@ mkdir -p "$dst"
curl --location "$src/facets.json" > "$dst/facets.json"
curl --location "$src/measures.json" > "$dst/measures.json"
curl --location "$src/productTypes.json" > "$dst/productTypes.json"
curl --location "$src/vocabulary.json" > "$dst/vocabulary.json"

View File

@@ -53,9 +53,15 @@ RSpec.describe AffiliateSalesQuery do
it "converts an array to a hash" do
row = [
"Apples",
"item", "item", nil, nil,
"item",
"item",
nil,
nil,
15.50,
"3210", "3211",
"3210",
"country1",
"3211",
"country2",
3,
]
expect(query.label_row(row)).to eq(
@@ -67,7 +73,9 @@ RSpec.describe AffiliateSalesQuery do
unit_presentation: nil,
price: 15.50,
distributor_postcode: "3210",
distributor_country: "country1",
supplier_postcode: "3211",
supplier_country: "country2",
quantity_sold: 3,
}
)

View File

@@ -9,6 +9,12 @@ RSpec.describe DfcIo do
let(:enterprise) do
DataFoodConsortium::Connector::Enterprise.new("Pete's Pumpkins")
end
let(:order) do
DataFoodConsortium::Connector::Order.new("https://example.net", orderStatus: orderstate.HELD)
end
let(:orderstate) do
DfcLoader.vocabulary("vocabulary").STATES.ORDERSTATE
end
describe ".export" do
it "exports nothing" do
@@ -33,5 +39,20 @@ RSpec.describe DfcIo do
*%w(@id @type dfc-b:affiliates)
)
end
it "recognises loaded vocabularies" do
json = DfcIo.export(order)
result = JSON.parse(json)
expect(result["dfc-b:hasOrderStatus"]).to eq "dfc-v:Held"
end
end
describe ".import" do
it "recognises loaded vocabularies" do
result = DfcIo.import(DfcIo.export(order))
expect(result.orderStatus).to eq orderstate.HELD
end
end
end

View File

@@ -21,4 +21,10 @@ RSpec.describe DfcLoader do
)
expect(result["dfc-b:name"]).to eq "Tomato"
end
it "loads vocabularies" do
terms = DfcLoader.vocabulary("vocabulary")
expect(terms.STATES.ORDERSTATE.HELD.semanticId)
.to eq "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Held"
end
end

View File

@@ -0,0 +1,450 @@
{
"@context": {
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"skos" : "http://www.w3.org/2004/02/skos/core#",
"dfc": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_FullModel.owl#",
"dc": "http://purl.org/dc/elements/1.1/#",
"dfc-b": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#",
"dfc-t": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_TechnicalOntology.owl#",
"dfc-m": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/measures.rdf#",
"dfc-pt": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#",
"dfc-f": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/facets.rdf#",
"dfc-v": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#",
"ontosec": "http://www.semanticweb.org/ontologies/2008/11/OntologySecurity.owl#",
"dfc-b:DFC_BusinessOntology_ObjectProperty": {
"@type": "@id"
},
"dfc-b:DFC_Interface_Property": {
"@type": "@id"
},
"dfc-b:addressOf": {
"@type": "@id"
},
"dfc-b:affiliatedTo": {
"@type": "@id"
},
"dfc-b:affiliates": {
"@type": "@id"
},
"dfc-b:allergenCharacteristicOf": {
"@type": "@id"
},
"dfc-b:belongsTo": {
"@type": "@id"
},
"dfc-b:brandOf": {
"@type": "@id"
},
"dfc-b:certificateOf": {
"@type": "@id"
},
"dfc-b:characteristicOf": {
"@type": "@id"
},
"dfc-b:claimOf": {
"@type": "@id"
},
"dfc-b:composedOf": {
"@type": "@id"
},
"dfc-b:composes": {
"@type": "@id"
},
"dfc-b:concernedBy": {
"@type": "@id"
},
"dfc-b:concerns": {
"@type": "@id"
},
"dfc-b:constituedBy": {
"@type": "@id"
},
"dfc-b:constitutes": {
"@type": "@id"
},
"dfc-b:consumedBy": {
"@type": "@id"
},
"dfc-b:consumes": {
"@type": "@id"
},
"dfc-b:containerInformationOf": {
"@type": "@id"
},
"dfc-b:coordinatedBy": {
"@type": "@id"
},
"dfc-b:coordinates": {
"@type": "@id"
},
"dfc-b:definedBy": {
"@type": "@id"
},
"dfc-b:defines": {
"@type": "@id"
},
"dfc-b:deliveredAt": {
"@type": "@id"
},
"dfc-b:facetOf": {
"@type": "@id"
},
"dfc-b:from": {
"@type": "@id"
},
"dfc-b:geographicalOriginOf": {
"@type": "@id"
},
"dfc-b:hasAddress": {
"@type": "@id"
},
"dfc-b:hasAllergenCharacteristic": {
"@type": "@id"
},
"dfc-b:hasAllergenDimension": {
"@type": "@id"
},
"dfc-b:hasBrand": {
"@type": "@id"
},
"dfc-b:hasCertification": {
"@type": "@id"
},
"dfc-b:hasCharacteristic": {
"@type": "@id"
},
"dfc-b:hasClaim": {
"@type": "@id"
},
"dfc-b:hasContainerInformation": {
"@type": "@id"
},
"dfc-b:hasDimension": {
"@type": "@id"
},
"dfc-b:hasFacet": {
"@type": "@id"
},
"dfc-b:hasFulfilmentStatus": {
"@type": "@id"
},
"dfc-b:hasGeographicalOrigin": {
"@type": "@id"
},
"dfc-b:hasIngredient": {
"@type": "@id"
},
"dfc-b:hasInput": {
"@type": "@id"
},
"dfc-b:hasLabellingCharacteristic": {
"@type": "@id"
},
"dfc-b:hasLabellingDimension": {
"@type": "@id"
},
"dfc-b:hasMainContact": {
"@type": "@id"
},
"dfc-b:hasNatureOrigin": {
"@type": "@id"
},
"dfc-b:hasNutrientCharacteristic": {
"@type": "@id"
},
"dfc-b:hasNutrientDimension": {
"@type": "@id"
},
"dfc-b:hasObject": {
"@type": "@id"
},
"dfc-b:hasOffer": {
"@type": "@id"
},
"dfc-b:hasOption": {
"@type": "@id"
},
"dfc-b:hasOrderStatus": {
"@type": "@id"
},
"dfc-b:hasOutput": {
"@type": "@id"
},
"dfc-b:hasPart": {
"@type": "@id"
},
"dfc-b:hasPartOrigin": {
"@type": "@id"
},
"dfc-b:hasPaymentMethod": {
"@type": "@id"
},
"dfc-b:hasPaymentStatus": {
"@type": "@id"
},
"dfc-b:hasPhoneNumber": {
"@type": "@id"
},
"dfc-b:hasPhysicalCharacteristic": {
"@type": "@id"
},
"dfc-b:hasPhysicalDimension": {
"@type": "@id"
},
"dfc-b:hasPrice": {
"@type": "@id"
},
"dfc-b:hasProcess": {
"@type": "@id"
},
"dfc-b:hasQuantity": {
"@type": "@id"
},
"dfc-b:hasReference": {
"@type": "@id"
},
"dfc-b:hasSocialMedia": {
"@type": "@id"
},
"dfc-b:hasStatus": {
"@type": "@id"
},
"dfc-b:hasTemperature": {
"@type": "@id"
},
"dfc-b:hasTransformationType": {
"@type": "@id"
},
"dfc-b:hasType": {
"@type": "@id"
},
"dfc-b:hasUnit": {
"@type": "@id"
},
"dfc-b:holds": {
"@type": "@id"
},
"dfc-b:hostedAt": {
"@type": "@id"
},
"dfc-b:hosts": {
"@type": "@id"
},
"dfc-b:identifiedBy": {
"@type": "@id"
},
"dfc-b:identifies": {
"@type": "@id"
},
"dfc-b:industrializedBy": {
"@type": "@id"
},
"dfc-b:industrializes": {
"@type": "@id"
},
"dfc-b:inputOf": {
"@type": "@id"
},
"dfc-b:isIngredientOf": {
"@type": "@id"
},
"dfc-b:isPriceOf": {
"@type": "@id"
},
"dfc-b:isTemperatureOf": {
"@type": "@id"
},
"dfc-b:labellingCharacteristicOf": {
"@type": "@id"
},
"dfc-b:listedIn": {
"@type": "@id"
},
"dfc-b:lists": {
"@type": "@id"
},
"dfc-b:localizedBy": {
"@type": "@id"
},
"dfc-b:localizes": {
"@type": "@id"
},
"dfc-b:mainContactOf": {
"@type": "@id"
},
"dfc-b:maintainedBy": {
"@type": "@id"
},
"dfc-b:maintains": {
"@type": "@id"
},
"dfc-b:managedBy": {
"@type": "@id"
},
"dfc-b:manages": {
"@type": "@id"
},
"dfc-b:natureOriginOf": {
"@type": "@id"
},
"dfc-b:nutrientCharacteristicOf": {
"@type": "@id"
},
"dfc-b:objectOf": {
"@type": "@id"
},
"dfc-b:offeredThrough": {
"@type": "@id"
},
"dfc-b:offers": {
"@type": "@id"
},
"dfc-b:offersTo": {
"@type": "@id"
},
"dfc-b:optionOf": {
"@type": "@id"
},
"dfc-b:orderedBy": {
"@type": "@id"
},
"dfc-b:orders": {
"@type": "@id"
},
"dfc-b:outputOf": {
"@type": "@id"
},
"dfc-b:ownedBy": {
"@type": "@id"
},
"dfc-b:owns": {
"@type": "@id"
},
"dfc-b:paidWith": {
"@type": "@id"
},
"dfc-b:partOf": {
"@type": "@id"
},
"dfc-b:partOriginOf": {
"@type": "@id"
},
"dfc-b:phoneNumberOf": {
"@type": "@id"
},
"dfc-b:physicalCharacteristicOf": {
"@type": "@id"
},
"dfc-b:pickedUpAt": {
"@type": "@id"
},
"dfc-b:processOf": {
"@type": "@id"
},
"dfc-b:producedBy": {
"@type": "@id"
},
"dfc-b:produces": {
"@type": "@id"
},
"dfc-b:proposedBy": {
"@type": "@id"
},
"dfc-b:proposes": {
"@type": "@id"
},
"dfc-b:referenceOf": {
"@type": "@id"
},
"dfc-b:referencedBy": {
"@type": "@id"
},
"dfc-b:references": {
"@type": "@id"
},
"dfc-b:refersTo": {
"@type": "@id"
},
"dfc-b:representedBy": {
"@type": "@id"
},
"dfc-b:represents": {
"@type": "@id"
},
"dfc-b:requestedBy": {
"@type": "@id"
},
"dfc-b:requests": {
"@type": "@id"
},
"dfc-b:satisfiedBy": {
"@type": "@id"
},
"dfc-b:satisfies": {
"@type": "@id"
},
"dfc-b:selectedBy": {
"@type": "@id"
},
"dfc-b:selects": {
"@type": "@id"
},
"dfc-b:sells": {
"@type": "@id"
},
"dfc-b:socialMediaOf": {
"@type": "@id"
},
"dfc-b:soldBy": {
"@type": "@id"
},
"dfc-b:storedIn": {
"@type": "@id"
},
"dfc-b:stores": {
"@type": "@id"
},
"dfc-b:suppliedBy": {
"@type": "@id"
},
"dfc-b:supplies": {
"@type": "@id"
},
"dfc-b:suppliesTo": {
"@type": "@id"
},
"dfc-b:to": {
"@type": "@id"
},
"dfc-b:tracedBy": {
"@type": "@id"
},
"dfc-b:traces": {
"@type": "@id"
},
"dfc-b:transformedBy": {
"@type": "@id"
},
"dfc-b:transforms": {
"@type": "@id"
},
"dfc-b:typeOf": {
"@type": "@id"
},
"dfc-b:uses": {
"@type": "@id"
},
"dfc-t:represent": {
"@type": "@id"
},
"dfc-t:hasPivot": {
"@type": "@id"
},
"dfc-t:hostedBy": {
"@type": "@id"
},
"dfc-t:owner": {
"@type": "@id"
}
}
}

View File

@@ -0,0 +1,351 @@
[ {
"@graph" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#",
"@type" : [ "http://www.w3.org/2002/07/owl#Ontology" ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Cancelled",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#OrderState"
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#PaymentState"
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#FulfilmentState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Cancelled"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Complete",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#OrderState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Complete"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary",
"@type" : [ "http://www.w3.org/2004/02/skos/core#ConceptScheme" ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "DFC_Vocabulary"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Draft",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#OrderState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Draft"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Fulfilled",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#FulfilmentState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Fulfilled"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#FulfilmentState",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#States"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Fulfilment state"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Held",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#OrderState"
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#FulfilmentState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Held"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#OrderState",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#States"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Order state"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Paid",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#PaymentState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Paid"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#PaymentState",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#States"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Payment state"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#States",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "States"
} ],
"http://www.w3.org/2004/02/skos/core#topConceptOf" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Unfulfilled",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#FulfilmentState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Unfulfilled"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#Unpaid",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#PaymentState"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Unpaid"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#accept",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "accept"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#c_734fc709",
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "FulfilmentState"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#combine",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "combine"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#consume",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "consume"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#dropoff",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "dropoff"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#lower",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "lower"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#modify",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "modify"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#move",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "move"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#pickup",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "pickup"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#produce",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "produce"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#raise",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "raise"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#separate",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "separate"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "Transformation type"
} ],
"http://www.w3.org/2004/02/skos/core#topConceptOf" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ]
}, {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#use",
"@type" : [ "http://www.w3.org/2004/02/skos/core#Concept" ],
"http://www.w3.org/2004/02/skos/core#broader" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#transformationType"
} ],
"http://www.w3.org/2004/02/skos/core#inScheme" : [ {
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#DFC_Vocabulary"
} ],
"http://www.w3.org/2004/02/skos/core#prefLabel" : [ {
"@language" : "en",
"@value" : "use"
} ]
} ],
"@id" : "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/vocabulary.rdf#"
} ]

View File

@@ -161,6 +161,11 @@ module OrderManagement
persist_totals
end
def update_voucher
VoucherAdjustmentsService.new(order).update
update_totals_and_states
end
private
def cancel_payments_requiring_auth

View File

@@ -460,6 +460,26 @@ module OrderManagement
end
end
describe "#update_voucher" do
let(:voucher_service) { instance_double(VoucherAdjustmentsService) }
it "calls VoucherAdjustmentsService" do
expect(VoucherAdjustmentsService).to receive(:new).and_return(voucher_service)
expect(voucher_service).to receive(:update)
updater.update_voucher
end
it "calls update_totals_and_states" do
allow(VoucherAdjustmentsService).to receive(:new).and_return(voucher_service)
allow(voucher_service).to receive(:update)
expect(updater).to receive(:update_totals_and_states)
updater.update_voucher
end
end
def update_order_quantity(order)
order.line_items.first.update_attribute(:quantity, 2)
end

View File

@@ -1,15 +0,0 @@
# frozen_string_literal: true
# Our error logging API currently wraps Bugsnag.
# It makes us more flexible if we wanted to replace Bugsnag or change logging
# behaviour.
module OpenFoodNetwork
module ErrorLogger
# Tries to escalate the error to a developer.
# If Bugsnag is configured, it will notify it. It would be nice to implement
# some kind of fallback.
def self.notify(error)
Bugsnag.notify(error)
end
end
end

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