Compare commits

...

259 Commits

Author SHA1 Message Date
Rachel Arnould
b5020cc740 Update en.yml
Removing mentions about UFC Que choisir and changing the link
2024-11-25 13:31:36 +01:00
filipefurtad0
7a2a6fab21 Update all locales with the latest Transifex translations 2024-11-22 16:36:15 -06:00
Filipe
0f4ca50d0e Merge pull request #12962 from bouaik/Scroll-to-top-when-using-pagination
Scroll to top when using pagination
2024-11-22 15:13:38 -06:00
Maikel
3ec8cd24d3 Merge pull request #12960 from mkllnk/dfc-link-backorder
Store link to open backorder
2024-11-22 10:22:43 +11:00
Maikel
d0dcc92ca7 Merge pull request #12976 from dacook/update-docker-readme
Update docker readme
2024-11-21 13:25:42 +11:00
Filipe
22f3afc7f7 Merge pull request #12930 from chahmedejaz/task/12878-add-variant-name-in-od-report
Report Orders and Distributors should display variant
2024-11-20 12:23:36 -06:00
Maikel Linke
46048dcd18 Handle empty backorder
Backorder can become empty after a customer cancels their order. Then we
don't want to fail but also don't need to place an order.
2024-11-19 15:53:59 +11:00
Maikel Linke
a8fb6492f4 Lookup backorder for updates with saved link 2024-11-19 15:53:59 +11:00
Maikel Linke
4610141ed8 Add shortcut to order's exchange 2024-11-19 15:53:59 +11:00
Maikel Linke
8098131dba Store link to open backorder
We don't use the link yet, but it's there.
2024-11-19 15:53:59 +11:00
Maikel Linke
597d9ad314 Add semantic links to Exchange 2024-11-19 15:53:59 +11:00
Maikel Linke
1ce0b25bb0 Switch SemanticLink to use new association
And ActiveRecord magic does the rest when used correctly.
2024-11-19 15:53:58 +11:00
Maikel Linke
c07ec6cdfd Polymorphically associate SemanticLinks to variant 2024-11-19 15:53:58 +11:00
Maikel Linke
48e8ad3dd0 Add subject column to semantic links 2024-11-19 15:53:58 +11:00
David Cook
60d4cd60ff Merge pull request #12972 from duleorlovic/12971_add_serbian_lang
Add Serbian lang: tx pull -l sr fix #12971
2024-11-19 09:46:59 +11:00
Ahmed Ejaz
d62d3041b4 12878: add relations in model 2024-11-18 11:45:02 +05:00
Ahmed Ejaz
42fc0f7230 12878: add model in migration 2024-11-18 11:13:02 +05:00
filipefurtad0
328aee6a03 Update all locales with the latest Transifex translations 2024-11-17 20:47:52 -06:00
Rachel Arnould
db79af45fb Merge pull request #12879 from chahmedejaz/task/12776-pay-suppliers-report
[Flower Farms] - Pay Suppliers Report
2024-11-15 11:36:29 +01:00
Ahmed Ejaz
ed7685222e 12776: fix included tax on fees 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
4965e2bb9a Update lib/reporting/reports/suppliers/helpers/line_items_access_helper.rb
Co-authored-by: David Cook <david@redcliffs.net>
2024-11-15 11:09:56 +01:00
Ahmed Ejaz
6b3b29ac39 12776: refactor spec 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
9bcdac8f30 12776: rename vat to tax 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
e2d999da8d 12776: use EnterpriseFeeCalculator in specs 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
bc57447d54 12776: refactor supplier_adjustments method 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
f3e086ad59 12776: remove unnecessary include 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
298c0e8d7f fix reported issues:
- wrong enterprise fees
- always 0 tax on fees
2024-11-15 11:09:56 +01:00
Ahmed Ejaz
ed559b5257 update specs to have more line items 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
1fbdf25296 12776: fix missing order numbers 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
ec0d2d346b use to_date for locale based formating 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
68c0d98736 add slash for abn acn 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
458c8f7608 Update lib/reporting/reports/suppliers/helpers/columns_helper.rb
Co-authored-by: David Cook <david@redcliffs.net>
2024-11-15 11:09:56 +01:00
Ahmed Ejaz
654263a823 add systems spec 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
77f9c6587c fix specs 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
13a614a5aa fix rubocop lines issue 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
add973f1ff 12776: add new line 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
4349e42a84 12776: add specs 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
7cb28fd064 12776: add supplier report 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
122a64e488 12776: add new report option 2024-11-15 11:09:56 +01:00
Ahmed Ejaz
39fa8e0ace 12878: fix migration class name 2024-11-14 11:04:28 +05:00
Maikel
8c6c1e28ff Merge pull request #12967 from rioug/fix-bugsnag-notify
Fix Bugsnag call to notify
2024-11-14 13:42:23 +11:00
David Cook
d9809fc1f4 Update docker readme
I think it was misleading, and considering the challenges everyone seems to have, we need to be more realistic.

[skip ci]
2024-11-14 09:56:59 +11:00
David Cook
889bec7404 Merge pull request #12961 from mkllnk/total-on-hand
Remove unused stock aggregation
2024-11-13 16:35:55 +11:00
David Cook
3f91027c51 Merge pull request #12953 from mkllnk/test-secret-collision
Avoid collision of test secrets
2024-11-13 15:58:41 +11:00
Gaetan Craig-Riou
2f9200b68b Per review, use a better name 2024-11-11 14:03:10 +11:00
filipefurtad0
5d9bb9a8d5 Update all locales with the latest Transifex translations 2024-11-10 18:03:57 -06:00
Filipe
7b677796c1 Merge pull request #12797 from rioug/report-fix-supplier
Fix supplier loading on Product & inventory report
2024-11-10 18:03:20 -06:00
Dusan Orlovic
528c851e89 Add Serbian lang: tx pull -l sr fix #12971 2024-11-10 20:43:13 +01:00
Gaetan Craig-Riou
eb66244b74 Fix Bugsnag call to notify
Make sure we add metadata as expected:
https://docs.bugsnag.com/platforms/ruby/rails/reporting-handled-errors/#add_metadata
2024-11-06 14:01:48 +11:00
Gaetan Craig-Riou
9afd545897 Merge pull request #12959 from openfoodfoundation/dependabot/npm_and_yarn/jquery-ui-1.14.1
Bump jquery-ui from 1.14.0 to 1.14.1
2024-11-04 10:25:26 +11:00
Gaetan Craig-Riou
36f7063897 Merge pull request #12958 from openfoodfoundation/dependabot/npm_and_yarn/elliptic-6.6.0
Bump elliptic from 6.5.7 to 6.6.0
2024-11-04 10:00:23 +11:00
Gaetan Craig-Riou
a53a697e66 Merge pull request #12956 from openfoodfoundation/dependabot/npm_and_yarn/trix-2.1.8
Bump trix from 2.1.7 to 2.1.8
2024-11-04 09:58:42 +11:00
Gaetan Craig-Riou
e9349ce79d Merge pull request #12955 from openfoodfoundation/dependabot/npm_and_yarn/floating-ui/dom-1.6.12
Bump @floating-ui/dom from 1.6.11 to 1.6.12
2024-11-04 09:57:39 +11:00
bouaik
8709c137c7 Scroll to top when using pagination 2024-11-01 11:39:41 +01:00
Maikel Linke
271475893d Remove unused stock aggregation 2024-11-01 16:46:35 +11:00
filipefurtad0
49a24ebd33 Update all locales with the latest Transifex translations 2024-10-31 19:05:25 -06:00
Filipe
a08b0a8b32 Merge pull request #12917 from nicogaldamez/ignore-name-column-for-customers
Ignores name column on customer model
2024-10-31 17:35:47 -06:00
Filipe
0d97f992b9 Merge pull request #12943 from mkllnk/sanitise
Sanitise HTML attributes in the database
2024-10-31 17:32:56 -06:00
Filipe
996d2f0d46 Merge pull request #12947 from mkllnk/staging-baseline
Add scripts to save and restore baseline data
2024-10-31 16:47:23 -06:00
dependabot[bot]
f01a33c545 Bump jquery-ui from 1.14.0 to 1.14.1
Bumps [jquery-ui](https://github.com/jquery/jquery-ui) from 1.14.0 to 1.14.1.
- [Release notes](https://github.com/jquery/jquery-ui/releases)
- [Commits](https://github.com/jquery/jquery-ui/compare/1.14.0...1.14.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-31 09:58:26 +00:00
dependabot[bot]
48c88d426e Bump elliptic from 6.5.7 to 6.6.0
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.7 to 6.6.0.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.7...v6.6.0)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-31 02:16:59 +00:00
dependabot[bot]
f646a30dca Bump trix from 2.1.7 to 2.1.8
Bumps [trix](https://github.com/basecamp/trix) from 2.1.7 to 2.1.8.
- [Release notes](https://github.com/basecamp/trix/releases)
- [Commits](https://github.com/basecamp/trix/compare/v2.1.7...v2.1.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-30 09:47:49 +00:00
dependabot[bot]
1e21939963 Bump @floating-ui/dom from 1.6.11 to 1.6.12
Bumps [@floating-ui/dom](https://github.com/floating-ui/floating-ui/tree/HEAD/packages/dom) from 1.6.11 to 1.6.12.
- [Release notes](https://github.com/floating-ui/floating-ui/releases)
- [Changelog](https://github.com/floating-ui/floating-ui/blob/master/packages/dom/CHANGELOG.md)
- [Commits](https://github.com/floating-ui/floating-ui/commits/@floating-ui/dom@1.6.12/packages/dom)

---
updated-dependencies:
- dependency-name: "@floating-ui/dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-30 09:47:37 +00:00
Maikel Linke
337113000f Avoid collision of test secrets
A test was failing locally because I have the OpenID client secret set
in my environment. And the dummy value was the same as another test key.
So it got replaced with the wrong value.
2024-10-30 16:03:29 +11:00
David Cook
3756e368c8 Merge pull request #12921 from rioug/12908-error-when-tax-refund
Add Bugsnag notification if we reach tax rate refund code
2024-10-30 14:51:40 +11:00
Gaetan Craig-Riou
54acc97fa1 Merge pull request #12951 from MichaelDimmitt/fix-typo
fix typo
2024-10-30 11:08:12 +11:00
michael
0f6f7b332c fix typo 2024-10-29 08:18:14 -04:00
Maikel
946471923a Merge pull request #12895 from dacook/update-release-template
Update release template
2024-10-29 16:11:18 +11:00
Maikel Linke
3f353690c7 Load staging baseline even if db in use 2024-10-29 12:44:46 +11:00
Maikel
b8822ee179 Merge pull request #12945 from mkllnk/dfc-amend-after-cancel
Amend backorder after cancellations
2024-10-29 11:26:18 +11:00
Gaetan Craig-Riou
701504fbb3 Merge pull request #12938 from mkllnk/restock
Spec restock after order cancellation
2024-10-28 09:56:01 +11:00
filipefurtad0
e9900ec1c7 Update all locales with the latest Transifex translations 2024-10-25 09:35:51 -06:00
Maikel Linke
8a0d9d99e5 Add scripts to save and restore baseline data 2024-10-25 15:07:39 +11:00
Maikel Linke
decf1e6f03 Move old Buildkite scripts to archive folder
We could delete them all but I want use some of their wisdom for new CI
scripts.
2024-10-25 14:23:33 +11:00
Maikel Linke
e0638b1765 Amend backorder after cancellations
The new job class blends code from the BackorderJob and the
CompleteBackorderJob for the specific case of adjusting quantities after
an order has been cancelled.

I would like to write a more general class which can be used for any
order amendmends but this was the quickest solution to cater for
currently running pilots.
2024-10-24 17:08:50 +11:00
Maikel Linke
a5f677f748 Create OidcAccount factory for simpler specs 2024-10-24 17:08:45 +11:00
Maikel Linke
63c83a19d6 Fix backorder spec with incomplete test data 2024-10-24 16:21:39 +11:00
Maikel
762e6ec568 Merge pull request #12940 from dacook/bug-12939
Bug 12939
2024-10-24 10:30:37 +11:00
Maikel Linke
d2e5087668 Remove redundant HTML sanitisation
We don't need to run the sanitiser each time we read an attribute. It's
a waste of time.
2024-10-24 08:47:11 +11:00
Maikel Linke
169e1cf288 Sanitise HTML attributes in the database 2024-10-24 08:47:11 +11:00
David Cook
45ca2961ec Avoid crash 2024-10-23 22:06:53 +11:00
David Cook
1d75aa45ef spec 2024-10-23 21:55:49 +11:00
David Cook
a123369f8d Merge pull request #12935 from mkllnk/dfc-doc-deterministic
Make DFC API docs deterministic
2024-10-23 16:59:55 +11:00
Maikel Linke
90589ae868 Spec restock after order cancellation 2024-10-23 16:35:59 +11:00
Maikel Linke
167a69d2ef Spec change in order state more precisely 2024-10-23 14:46:12 +11:00
Maikel Linke
09524e266f Fix method name description 2024-10-23 14:42:02 +11:00
Maikel
1c58b061b4 Merge pull request #12934 from rioug/fix-unit-price-spec
Use the correct spanish translation
2024-10-22 10:21:26 +11:00
Gaetan Craig-Riou
24df29ddf5 Use the correct spanish translation
Translation has been updated so we need to use the correct spanish
word
2024-10-22 10:10:00 +11:00
drummer83
9f084057a1 Update all locales with the latest Transifex translations 2024-10-21 11:11:39 +02:00
Gaetan Craig-Riou
3f22e8cca7 Fix Bugsnag payload data 2024-10-21 11:13:42 +11:00
Konrad
c3b5456433 Merge pull request #12912 from chahmedejaz/task/12911-remove-admin-v3-toggle
Remove the admin_style_v3 toggle from Production and Staging environments
2024-10-19 13:41:14 +02:00
Konrad
7b0519dab9 Merge pull request #12927 from mkllnk/report-dates
Filter reports by last 3 months by default
2024-10-19 13:12:20 +02:00
Ahmed Ejaz
355541e8de Update db/migrate/20241011071014_update_item_name_to_product_in_od_report.rb
Co-authored-by: David Cook <david@redcliffs.net>
2024-10-18 12:32:14 +05:00
Ahmed Ejaz
e10c3dc59b add specs for migration 2024-10-18 12:31:05 +05:00
Maikel
2609298d88 Merge pull request #12929 from mkllnk/dfc-backorder-fix
Handle case of BackorderJob having no work
2024-10-18 10:04:36 +11:00
Maikel
783de09987 Merge pull request #12932 from mkllnk/dfc-offline-token
Request offline access when connecting OIDC account
2024-10-18 10:03:25 +11:00
Maikel Linke
7b8aeb7ef8 Request offline access when connecting OIDC account 2024-10-18 09:39:49 +11:00
Maikel Linke
9c7105e764 Handle case of BackorderJob having no work 2024-10-17 15:39:32 +11:00
Maikel
afff200680 Merge pull request #12923 from openfoodfoundation/dependabot/npm_and_yarn/trix-2.1.7
Bump trix from 2.1.6 to 2.1.7
2024-10-17 15:26:18 +11:00
Maikel Linke
6c431d4052 Fixup specs to use the new datepicker tools 2024-10-17 15:08:02 +11:00
Maikel Linke
2b8487cc6d Parse given datetime for reports properly 2024-10-17 15:08:02 +11:00
Maikel Linke
b9a72381fc Fix datepicker infinite loop
The new default dates were not aligned with the assumption that the
datepicker would open on the current date. The datepicker helper would
try to navigate to the previous month or next month in relation to the
reference date. Now with the wrong reference date, it would infinitely
go into the past or future, not finding the right year and month.

I chose a more robust approach of setting the year and month directly
which the user can do as well. Then we don't need a reference date.
2024-10-17 15:06:56 +11:00
Maikel Linke
ea8e925077 Show default date range to user in date picker 2024-10-17 13:16:12 +11:00
Maikel Linke
a13e5ced3d Select default dates for Packing report, too 2024-10-17 13:16:12 +11:00
Maikel Linke
aa7fffa5a2 Filter reports by last 3 months by default
The values are not shown on the screen and the user doesn't know which
default dates are applied but the filtering works.
2024-10-17 13:16:12 +11:00
Maikel Linke
3227922c76 Use reports helper to DRY 2024-10-17 13:16:12 +11:00
Maikel Linke
aa2a5757ec Move Customers report spec to own file 2024-10-17 13:16:12 +11:00
David Cook
197363b199 Merge pull request #12924 from openfoodfoundation/dependabot/npm_and_yarn/hotwired/turbo-8.0.12
Bump @hotwired/turbo from 8.0.11 to 8.0.12
2024-10-17 09:49:22 +11:00
Konrad
a023443c75 Merge pull request #12880 from rioug/5574-fix-checkout-order-total-calc
Fix checkout order total and payment fees calculation
2024-10-16 21:16:34 +02:00
Nicolás Galdámez
2e29426834 Deletes failing test
It was a test associated with the migration from name to first_name +
last_name
2024-10-16 08:32:18 -03:00
Nicolás Galdámez
8e4d306901 Ignores name column on customer model
It's not being used because now there are columns for first name and
last name
2024-10-16 08:32:18 -03:00
dependabot[bot]
38196e8ff3 Bump @hotwired/turbo from 8.0.11 to 8.0.12
Bumps [@hotwired/turbo](https://github.com/hotwired/turbo) from 8.0.11 to 8.0.12.
- [Release notes](https://github.com/hotwired/turbo/releases)
- [Commits](https://github.com/hotwired/turbo/commits)

---
updated-dependencies:
- dependency-name: "@hotwired/turbo"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 09:12:53 +00:00
dependabot[bot]
475c9fb4ab Bump trix from 2.1.6 to 2.1.7
Bumps [trix](https://github.com/basecamp/trix) from 2.1.6 to 2.1.7.
- [Release notes](https://github.com/basecamp/trix/releases)
- [Commits](https://github.com/basecamp/trix/compare/v2.1.6...v2.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 09:12:41 +00:00
Ahmed Ejaz
c48162388c 12911: remove admin_style_v3 toggle for prod and staging 2024-10-16 13:14:42 +05:00
Gaetan Craig-Riou
f024aff45d Add Bugsnag notification if we reach tax rate refund code
The original Spree code allow for a tax adjustment to be considered a
refund in a specific scenario:
- instance is using inclusive tax
- instance that applies different tax rate in different tax zones

This scenario should not happen with how our instances are configured
More info: https://github.com/openfoodfoundation/openfoodnetwork/pull/6565#discussion_r566535431
2024-10-16 11:37:21 +11:00
Maikel
ed668ded0a Merge pull request #12913 from mkllnk/dfc-import-items
Import product's invalid weight as 1 item
2024-10-16 10:47:12 +11:00
Gaetan Craig-Riou
b461d499ad Merge pull request #12914 from mkllnk/remove-stock-location-from-return-authorization
Remove StockLocation from ReturnAuthorization
2024-10-16 10:26:09 +11:00
Gaetan Craig-Riou
c1c281122f Merge pull request #12898 from dacook/buu-producer-specs
[BUU] Add missing specs
2024-10-16 10:05:20 +11:00
David Cook
8c4cc051a4 Merge pull request #12916 from openfoodfoundation/dependabot/npm_and_yarn/hotwired/turbo-8.0.11
Bump @hotwired/turbo from 8.0.10 to 8.0.11
2024-10-16 10:01:50 +11:00
David Cook
d5b2408947 Reveal un-implemented tests 2024-10-16 09:51:04 +11:00
Maikel Linke
1eb70370c7 Import product's invalid weight as 1 item
We previously stored a scale which made the product screen believe that
we are dealing with weight.
2024-10-16 09:27:49 +11:00
Filipe
97b6289263 Merge pull request #12787 from rioug/move-variant-unit-attributes-to-variant
[Product Refactor] Move variant unit sizes to variant
2024-10-15 19:58:45 +01:00
dependabot[bot]
bda28dfaf7 Bump @hotwired/turbo from 8.0.10 to 8.0.11
Bumps [@hotwired/turbo](https://github.com/hotwired/turbo) from 8.0.10 to 8.0.11.
- [Release notes](https://github.com/hotwired/turbo/releases)
- [Commits](https://github.com/hotwired/turbo/compare/v8.0.10...v8.0.11)

---
updated-dependencies:
- dependency-name: "@hotwired/turbo"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 09:13:53 +00:00
Maikel Linke
d1ebe4e1d1 Make DFC API docs deterministic 2024-10-15 15:47:31 +11:00
Maikel Linke
a64aea4b9c Remove StockLocation from ReturnAuthorization
We have only one default location and don't need it associated to
anything.
2024-10-15 13:36:57 +11:00
David Cook
cc9b764f0f Fix rubocop 2024-10-15 10:37:20 +11:00
David Cook
ac5fa21ff2 Clean up 2024-10-15 10:33:20 +11:00
Gaetan Craig-Riou
781fcf21b9 Merge pull request #12910 from openfoodfoundation/dependabot/npm_and_yarn/jasmine-core-5.4.0
Bump jasmine-core from 5.3.0 to 5.4.0
2024-10-15 10:22:30 +11:00
Gaetan Craig-Riou
56d2642191 Merge pull request #12889 from openfoodfoundation/dependabot/npm_and_yarn/trix-2.1.6
Bump trix from 2.1.5 to 2.1.6
2024-10-15 10:20:43 +11:00
Rachel Arnould
f54552f939 Merge pull request #12886 from rioug/12855-VINE-connected-app
[Citi OFN Voucher] Add VINE connected app
2024-10-14 15:32:09 +02:00
dependabot[bot]
fb5740b38b Bump trix from 2.1.5 to 2.1.6
Bumps [trix](https://github.com/basecamp/trix) from 2.1.5 to 2.1.6.
- [Release notes](https://github.com/basecamp/trix/releases)
- [Commits](https://github.com/basecamp/trix/compare/v2.1.5...v2.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 09:40:36 +00:00
dependabot[bot]
db14080a7f Bump jasmine-core from 5.3.0 to 5.4.0
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.3.0 to 5.4.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.3.0...v5.4.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-10-14 09:27:08 +00:00
Gaetan Craig-Riou
01337c12f0 Post rebase, fix product cloning spec 2024-10-14 15:02:35 +11:00
Gaetan Craig-Riou
f8eeca856e Fix invoice print specs 2024-10-14 15:02:35 +11:00
Gaetan Craig-Riou
67c11333f3 Use AdminTooltipComponent, instead of partial 2024-10-14 15:02:35 +11:00
Gaetan Craig-Riou
40afe7e0ab Fix rebase issue 2024-10-14 15:02:34 +11:00
Gaetan Craig-Riou
ef1f3207f7 Update spec/system/admin/products_v3/update_spec.rb
Co-authored-by: Maikel <maikel@email.org.au>
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
b0433bd8f5 Re add test for invalid cloned products
There is no easy way to make the original product invalid, but we can
make sure the cloned product will be invalid. The cloned product add
"COPe OF " in front of the product's name, so by starting with a name
that's long enough, the cloned product will have a name longer that 255
char and will then be invalid.
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
755a394704 Fix spec to remove reliance on browser's message
Client side validation messages depend on the browser's locale, which
we have no controll over. Now we just check a message is set.
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
04e14bf38b Per review, check value are saved in the database 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
ce0c7929a7 Per review, remove the use of raw 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
2a671d491d Remove commented out code 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
7c2c614f90 Update spec/models/spree/variant_spec.rb
Co-authored-by: David Cook <david@redcliffs.net>
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
3bb2232bc1 Remove non updatable check when updating a product
After the product redactor it only checked for the "description" on
product, which is actually skipped when doing an update.
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
377f035ea8 Fix bulk coop report
The current spec is useless, but it has been addressed on master
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
dbca2e2b56 Add all columns moved to variant to ignored_columns 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
0695b434a2 Fix rebase issue 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
9db417319d Improve variant related validation when creating product
I disabled Metrics/AbcSize for ensure_standard_variant as I don't think
that's hard to understand the code. And utimately it will be removed
once product actually becomes optional.
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
a500c75ee9 Add stying for the unit pop out 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
630c398b12 Move unit popout css to a partial 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
64f60d1c8c Fix small bug on edit variant page
- make sure the weight is only cleared when needed
- make sure the displayed unit is up to date
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
218d07c90d Fix product import controller 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
83a619b097 Fix bulk order management page 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
fa986f3fc2 Fix orders and fulfillment report 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
977b6e6c2a Fix minor differences in local env and CI 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
f7446749ff Fix Unit price system spec 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
844cab458e Post rebase fix product import system spec 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
8ec1f61cd7 Fix legacy bulk edit product system spec 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
893b541dca Fix product system spec
The pending spec are to be fix after a rebase, master currently as
some changes which will make this easier
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
4ae392490b Fix variant system spec 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
cda57fdb44 Add toggleOnHand action
It replicate the behavior of setOnDemand angular directive
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
25171413ef Update Spree::Price parsing to match LocalizedNumber.parse
Spree::Price parsing was returning 0.0 when given a an empty string as
price, resulting in a variant being valid even if no price was given. It
only happened if `Spree::LocalizedNumber` wasn't used.
Spree::LocalizedNumber` return nil if given a blank number.
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
4ad6971121 Fix Bulk product edit system spec after rebase 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
8f38762393 Add missing translations for variant form 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
d55950a3c5 Fix rebase issue 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
45075a0ccd Fix rebase typo 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
144a09916c Use instance_double instead of double
Instance double, amongst other thing,  verifies that any methods being
stubbed would be present on an instance of the class
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
00dfe6810f Fix ProductDuplicator
There isn't away I could think of to create an invalid product, so I
removed those test
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
058d7eeb69 Use unit_value_with_description 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
324a4ff591 Backport fix for hungarian instance 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
7f16b6acde Update variant form and rip out angular 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
ce268ec175 Add systemOfMeasurement to VariantUnitManager 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
cc85fed7cc Add localizeCurrency and specs 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
45b0686130 Add PriceParser and UnitPrices and specs
This is in preparation for removing angular from the variant update
page.

Converted using  https://www.codeconvert.ai/coffeescript-to-javascript-converter
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
4cd83d3fd4 Prettify javascript
Also update .prettierignore so that spec files get prettified as well
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
768825d689 Fix Bulk Edit product part 2
Note, the empty entry for unit scale need a css fix , currently showing
at half height
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
e8234ee4a0 Fix Bulk products edit page , part 1 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
94030527a4 Update jest configuration to include webpacker dir
This allows the test environment to correctly resolve import of
services in controller ie: `import OptionValueNamer from
"js/services/option_value_namer";`

The added benefit is we can now import package to be tested directly
without having to specify the whole relative path ie in test file you
can do : `import variant_controller from "controllers/variant_controller";`
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
6ff9650eaf Fix legacy bulk edit products UI 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
b1b534aa1b Fix product and variant api serializer 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
cd74a73680 Consolidate angular option value namer spec
Merge the two spec files into the correct one.
2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
36c4d24c93 Fix angular option value namer 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
9b4cd014bf Fix DFC supplied product builder 2024-10-14 15:01:18 +11:00
Gaetan Craig-Riou
c8bf23bdc2 Fix UnitPrice spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
df82dd0759 Fix API v0 variants controller spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
5ec39f994a Fix spree admin products controller spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
8a31153d6d Fix API v0 products controller spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
4109fbde70 Fix variant controller spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
37ae217afc Fix product set spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
4fd115897a Refactor ProductImport::EntryValidator
Move comparaison function to ProductImport::SpreadsheetEntry
2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
e22804712e Fix product importer 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
d7d253e58d Fix Unit Price service 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
e2c762f06b Refactor, use instance_double in variant spec 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
1ad7123a9d Fix Spree::LineItem 2024-10-14 14:56:47 +11:00
Gaetan Craig-Riou
1793aa3532 Migrate unit sizes to variant 2024-10-14 14:56:46 +11:00
Gaetan Craig-Riou
d0fe1585d7 Move variant unit attributes to variant 2
Update Spree::Product and spec
2024-10-14 14:56:46 +11:00
Gaetan Craig-Riou
f58a3a859f Move variant unit attributes to variant 1
Update Spree::Variant model and spec
2024-10-14 14:56:46 +11:00
Gaetan Craig-Riou
3b89cd5957 Fix option value namer
Uses the variant  variant_unit, variant_unit_name, variant_unit_scale
2024-10-14 14:56:46 +11:00
Gaetan Craig-Riou
e33ed5141b Fix weigths and measures
Use variant_unit, variant_unit_scale from the variant
2024-10-14 14:56:46 +11:00
Gaetan Craig-Riou
4d81b145ca Add variant_unit_scale, variant_unit_name to variant 2024-10-14 14:56:46 +11:00
Konrad
7211b0d64a Merge pull request #12897 from rioug/12891-product-preview-fix-price
[Product Preview] Fix price currency display
2024-10-12 18:14:01 +02:00
Ahmed Ejaz
641b7beee3 add specs 2024-10-12 04:47:45 +05:00
Ahmed Ejaz
60e8db9adc 12878: add migration to show new column product column 2024-10-12 03:52:00 +05:00
Ahmed Ejaz
b7285e48b3 12878: update unit to full_name to display in variant column 2024-10-12 03:50:11 +05:00
Maikel
52c1491b15 Merge pull request #12906 from mkllnk/dfc-stock-import
Fail gracefully on DFC product import errors
2024-10-11 13:50:09 +11:00
Maikel Linke
95ff0d8d4a Fail gracefully on DFC product import errors 2024-10-11 12:10:23 +11:00
Maikel Linke
7d2d14320f Spec that connector update fixed bug 2024-10-11 09:54:29 +11:00
Maikel
7d1551ed04 Merge pull request #12904 from mkllnk/dfc-connector
Bump DFC connector from 1.0.0.pre.alpha.12 to 1.0.0.pre.alpha.13
2024-10-11 09:52:45 +11:00
Maikel Linke
3e71459346 Update API doc 2024-10-11 09:15:42 +11:00
Maikel Linke
ce2c80283c Bump datafoodconsortium-connector from 1.0.0.pre.alpha.12 to 1.0.0.pre.alpha.13
Changed

- Use nil as default value for all types except arrays.
2024-10-11 09:12:40 +11:00
Maikel Linke
2be3f7b86d Update all locales with the latest Transifex translations 2024-10-10 17:08:38 +11:00
Maikel
2d975c5534 Merge pull request #12899 from mkllnk/dfc-stock-check
DFC Orders update for pilot 1 and 2
2024-10-10 17:06:18 +11:00
Maikel Linke
86c91143b7 Update more variant data on import 2024-10-10 16:59:04 +11:00
Maikel Linke
cde757efbd Split growing class 2024-10-10 16:58:01 +11:00
Maikel Linke
260e7ba817 Update products when importing them multiple times
Instead of creating a new variant every time.
2024-10-10 16:57:58 +11:00
Maikel Linke
bda506528f Fix import of zero-weight products
We don't allow variants to have zero weight or volume. But a DFC import
in production showed that some catalogs list products with zero weight.
Despite the products having a weight, it's simpler to treat these as
items.
2024-10-10 14:08:02 +11:00
Maikel Linke
e429cb7198 Style growing class 2024-10-10 14:06:42 +11:00
Maikel Linke
a838ef4a21 DRY DFC product import 2024-10-10 14:04:54 +11:00
Maikel Linke
f0b6403c1d Fix locally flaky spec around date filters
This spec would fail on Australian systems early in the morning or in
other timezones accordingly.
2024-10-10 09:58:01 +11:00
Maikel Linke
71ca292c92 Synchronise stock with DFC catalog during checkout
This will delay the checkout request by a few seconds if there's stock
to sync. But we minimise the chance of missing reduced stock from orders
on another platform.

We still have a gap between the checkout and placing a backorder. In
that time we can't guarantee enough stock. But let's tackle that after
the pilot.
2024-10-09 14:47:07 +11:00
David Cook
bc87c98e92 Add some specs for the producer dropdowns 2024-10-09 13:02:39 +11:00
Gaetan Craig-Riou
5b8e0d734f Use Spree::Money to display prices
This is to ensure the correct currency and currency configuration is
applied.
2024-10-09 11:02:24 +11:00
David Cook
216883101e Update release template
[skip ci]
2024-10-08 21:02:02 +11:00
Maikel Linke
adf0340153 Remove duplicate method
The method `CheckoutCallbacks#valid_order_line_items?` was a duplicate
of `OrderStockCheck#valid_order_line_items?`.

Apparently, it had been extracted twice:

 * 1d074c2151
 * 06eb98bdf4

But the first commit duplicated the method while the second moved the
original declaration.
2024-10-08 16:57:36 +11:00
Maikel Linke
664f324db6 Sync stock of multiple linked catalogs
And the logic becomes a bit simpler.
2024-10-08 16:37:35 +11:00
Gaetan Craig-Riou
08308ba08e Fix spec checking if VINE api is set up
The condition for checking the error now match a real scenario
2024-10-08 16:15:35 +11:00
Maikel Linke
c609107379 Avoid race condition between checkout and stock sync 2024-10-08 16:03:10 +11:00
Gaetan Craig-Riou
df67b53971 Re add VINE_API_URL env variable
And add error handling if the variable is not set
2024-10-08 13:26:57 +11:00
Maikel
6f2c5b5f7f Merge pull request #12888 from mkllnk/dfc-stock
[DFC Orders] Backorder stock controlled products
2024-10-08 10:57:59 +11:00
Gaetan Craig-Riou
a3d8ae693d Add encryption for ConnectedApps::Vine#data
Added layer of security, we encrypt the API key and related secret.
It requires setting up some encryption keys that can be generated wiht
`bin/rails db:encryption:init`
2024-10-07 15:09:58 +11:00
Gaetan Craig-Riou
b14a1e72f3 Handle api secret
The VINE Api require a secret and an API key to be used. The secret is
used to sign the request. The secret is linked to the API key so we need
to store it along side the key.
2024-10-07 15:09:58 +11:00
Gaetan Craig-Riou
224738e0a1 Per review, clean up code 2024-10-07 15:09:51 +11:00
Gaetan Craig-Riou
10c3c53aad Fix translation per review. 2024-10-07 11:23:22 +11:00
Gaetan Craig-Riou
e5b7f89b32 Merge pull request #12887 from mkllnk/stock-cleanup2
Remove unneeded StockLocation code
2024-10-07 09:40:46 +11:00
Maikel Linke
61aa02b3c3 Sync stock with DFC catalog after cart update 2024-10-04 16:25:17 +10:00
Maikel Linke
4b2099625c Clarify method action with name
Thanks, David.
2024-10-04 14:34:17 +10:00
Maikel Linke
f8bd0a1cc7 Adjust backorder for stock controlled items
We aggregate quantities over the whole order cycle to account for
cancelations and order adjustments by admins.
2024-10-03 15:58:53 +10:00
Maikel Linke
09de223c93 Backorder stock controlled products 2024-10-03 13:30:16 +10:00
Maikel Linke
74c80c9fff Prepare BackorderJob for stock controlled items
We want to trigger the backordering for any linked product now. So let's
do that check early and then select the variants in the background.
It means less data passed to the job and less space for race conditions.
2024-10-03 13:28:20 +10:00
Maikel Linke
11f3bbc566 Remove leftover recording 2024-10-03 13:28:17 +10:00
Maikel Linke
e5ee398f26 Re-use default stock location in specs 2024-10-03 08:24:16 +10:00
Maikel Linke
99c098f567 Ignore StockLocation#active, it's always active 2024-10-03 08:24:16 +10:00
Maikel Linke
4b1d7d8a41 Remove dead permission to access StockLocation
We don't have any UI to edit stock locations. So this ability is unused.
2024-10-03 08:24:15 +10:00
Maikel Linke
1e3c18f3f6 Remove unneeded method StockLocation#propagate_variant 2024-10-03 08:24:15 +10:00
Gaetan Craig-Riou
22428fc78d ConnectedApps controller, handle ConnectedApps::Vine
Add logiv to connect and disconnect VINE API plus spec
2024-10-02 16:44:27 +10:00
Gaetan Craig-Riou
f980cb45f6 Add logic for ConnectedApps::Vine#connect and disconnect 2024-10-02 16:44:27 +10:00
Gaetan Craig-Riou
097c6dee2f Add VineApiService and specs
It handles connection to the VINE API
2024-10-02 16:44:21 +10:00
Gaetan Craig-Riou
1a30cf6495 Hide VINE token 2024-10-02 16:19:03 +10:00
Gaetan Craig-Riou
f7708d69a7 Add VineJwtService
Generate a JWT token to be used to connect to the VINE api
2024-10-02 16:16:28 +10:00
Gaetan Craig-Riou
aa5feb6605 Remove system spec
It's covered by unit test of order updater
2024-10-02 09:33:02 +10:00
Gaetan Craig-Riou
b2b6847882 Fix test data
The future is now ! :D
2024-10-01 10:38:33 +10:00
Gaetan Craig-Riou
d01d312b4f Fix updating pending payment
Check if payment actually have an adjustment before trying to update it
2024-10-01 10:22:47 +10:00
Gaetan Craig-Riou
a74cf97083 Fix spec when adding a product with transaction fee
Previous iteration did not actually check the payment fee had been
updated. It also checks the order total get correctly updated.
Spec is passing, so fixing the order updater also fix this bug
: https://github.com/openfoodfoundation/openfoodnetwork/issues/12512
2024-10-01 09:49:44 +10:00
Gaetan Craig-Riou
03dbd54b25 Fix order updater to update payment fees
The order updater did not take into account payment fees on pending
payment.
2024-09-30 16:15:59 +10:00
Gaetan Craig-Riou
fafd86a2db Revert change made in https://github.com/openfoodfoundation/openfoodnetwork/pull/12538
Although the change fix the issue in the back office scenario, it has
the side effect of getting the order total out of sync. Updating a
payment adjustment need to be followed by udpating the order total and
payment amount to keep everything in sync.
2024-09-30 16:04:44 +10:00
Gaetan Craig-Riou
eb8050d61d Add spec reproducing the bug 2024-09-25 15:58:28 +10:00
Gaetan Craig-Riou
0824430da5 Add Vine connected app
The connection/disconnection logic is yet to be implemented
2024-09-24 10:43:55 +10:00
Gaetan Craig-Riou
ef6e37e7ca Fix suppliers_of_products_distributed_by
Plus spec

Left over from product refactor, it was missed because it's not covered
by unit or integration test
2024-08-21 13:05:34 +10:00
Gaetan Craig-Riou
ffc2fed9b5 Remove unused code 2024-08-20 16:43:22 +10:00
296 changed files with 12606 additions and 4160 deletions

9
.env
View File

@@ -61,3 +61,12 @@ SMTP_PASSWORD="f00d"
# NEW_RELIC_AGENT_ENABLED=true
# NEW_RELIC_APP_NAME="Open Food Network"
# NEW_RELIC_LICENSE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Database encryption configuration, required for VINE connected app
# Generate with bin/rails db:encryption:init
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# VINE API settings
# VINE_API_URL="https://vine-staging.openfoodnetwork.org.au/api/v1"

View File

@@ -24,3 +24,8 @@ SITE_URL="0.0.0.0:3000"
RACK_TIMEOUT_SERVICE_TIMEOUT="0"
RACK_TIMEOUT_WAIT_TIMEOUT="0"
RACK_TIMEOUT_WAIT_OVERTIME="0"
# Database encryption configuration, required for VINE connected app
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="dev_primary_key"
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="dev_determinnistic_key"
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="dev_derivation_salt"

View File

@@ -16,5 +16,9 @@ STRIPE_PUBLIC_TEST_API_KEY="bogus_stripe_publishable_key"
SITE_URL="test.host"
OPENID_APP_ID="test-provider"
OPENID_APP_SECRET="12345"
OPENID_APP_SECRET="dummy-openid-app-secret-token"
OPENID_REFRESH_TOKEN="dummy-refresh-token"
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="test_primary_key"
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="test_deterministic_key"
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="test_derivation_salt"

View File

@@ -7,7 +7,7 @@ assignees: ''
---
## 1. Preparation on Thursday
## 1. Drafting on Friday
- [ ] Merge pull requests in the [Ready To Go] column
- [ ] Include translations: `script/release/update_locales`
@@ -26,8 +26,9 @@ assignees: ''
- [ ] Move this issue to Test Ready.
- [ ] Notify `@testers` in [#testing].
- [ ] Test build: [Deploy to Staging] with release tag.
- [ ] Notify a deployer to deploy it
## 3. Finish on Tuesday
## 3. Deployment at beginning of week
- [ ] Publish and notify [#global-community] (this is automatically posted with a plugin)
- [ ] Deploy the new release to all managed instances.
@@ -40,7 +41,7 @@ assignees: ''
</details>
- [ ] Notify [#instance-managers]:
> @instance_managers The new release has been deployed.
- [ ] [Create issue] for next release and confirm with next release manager in [#core-devs].
- [ ] [Create issue] for next release and confirm with next release drafter in [#delivery-circle].
The full process is described at https://github.com/openfoodfoundation/openfoodnetwork/wiki/Releasing.
@@ -53,5 +54,5 @@ The full process is described at https://github.com/openfoodfoundation/openfoodn
[Deploy to Staging]: https://github.com/openfoodfoundation/openfoodnetwork/actions/workflows/stage.yml
[#global-community]: https://app.slack.com/client/T02G54U79/C59ADD8F2
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
[#core-devs]: https://openfoodnetwork.slack.com/archives/GK2T38QPJ
[#delivery-circle]: https://openfoodnetwork.slack.com/archives/C01T75H6G0Z
[Transifex Client]: https://developers.transifex.com/docs/cli

View File

@@ -5,6 +5,7 @@
*.yaml
*.json
*.html
**/*.rb
# JS
# Enabled: app/webpacker/controllers/*.js and app/webpacker/packs/*.js
@@ -27,6 +28,5 @@ postcss.config.js
/coverage/
/engines/
/public/
/spec/
/tmp/
/vendor/

View File

@@ -31,7 +31,7 @@ This project needs specific ruby/bundler versions as well as node/yarn specific
* Install or change your Ruby version according to the one specified at [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file.
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/).
* Install [nodenv](https://github.com/nodenv/nodenv) to ensure the correct [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) is used.
- [nodevn](https://github.com/nodenv/nodenv) is recommended as a node version manager.
- [nodenv](https://github.com/nodenv/nodenv) is recommended as a node version manager.
* PostgreSQL database
* Redis (for background jobs)
* Chrome (for testing)

View File

@@ -246,7 +246,7 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
datafoodconsortium-connector (1.0.0.pre.alpha.12)
datafoodconsortium-connector (1.0.0.pre.alpha.13)
virtual_assembly-semantizer (~> 1.0, >= 1.0.5)
date (3.3.4)
debug (1.9.2)
@@ -379,13 +379,14 @@ GEM
bindata
faraday (~> 2.0)
faraday-follow_redirects
json-ld (3.3.1)
json-ld (3.3.2)
htmlentities (~> 4.3)
json-canonicalization (~> 1.0)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15)
rack (>= 2.2, < 4)
rdf (~> 3.3)
rexml (~> 3.2)
json-schema (4.1.1)
addressable (>= 2.8)
json_spec (1.1.5)

View File

@@ -187,9 +187,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
product.variants.length > 0
$scope.hasUnit = (product) ->
product.variant_unit_with_scale?
$scope.hasUnit = (variant) ->
variant.variant_unit_with_scale?
$scope.variantSaved = (variant) ->
variant.hasOwnProperty('id') && variant.id > 0
@@ -242,32 +241,28 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$window.location = destination
$scope.packProduct = (product) ->
if product.variant_unit_with_scale
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
product.variant_unit = match[1]
product.variant_unit_scale = parseFloat(match[2])
else
product.variant_unit = product.variant_unit_with_scale
product.variant_unit_scale = null
else
product.variant_unit = product.variant_unit_scale = null
if product.variants
for id, variant of product.variants
$scope.packVariant product, variant
$scope.packVariant variant
$scope.packVariant = (product, variant) ->
$scope.packVariant = (variant) ->
if variant.variant_unit_with_scale
match = variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
variant.variant_unit = match[1]
variant.variant_unit_scale = parseFloat(match[2])
else
variant.variant_unit = variant.variant_unit_with_scale
variant.variant_unit_scale = null
if variant.hasOwnProperty("unit_value_with_description")
match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/)
if match
product = BulkProducts.find product.id
variant.unit_value = parseFloat(match[1].replace(",", "."))
variant.unit_value = null if isNaN(variant.unit_value)
if variant.unit_value && product.variant_unit_scale
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, product.variant_unit_scale, 2))
if variant.unit_value && variant.variant_unit_scale
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, variant.variant_unit_scale, 2))
variant.unit_description = match[3]
$scope.incrementLimit = ->
@@ -321,13 +316,6 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("price")
filteredProduct.price = product.price
hasUpdatableProperty = true
if product.hasOwnProperty("variant_unit_with_scale")
filteredProduct.variant_unit = product.variant_unit
filteredProduct.variant_unit_scale = product.variant_unit_scale
hasUpdatableProperty = true
if product.hasOwnProperty("variant_unit_name")
filteredProduct.variant_unit_name = product.variant_unit_name
hasUpdatableProperty = true
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_hand = product.on_hand
hasUpdatableProperty = true
@@ -383,6 +371,14 @@ filterSubmitVariant = (variant) ->
if variant.hasOwnProperty("producer_id")
filteredVariant.supplier_id = variant.producer_id
hasUpdatableProperty = true
if variant.hasOwnProperty("variant_unit_with_scale")
filteredVariant.variant_unit = variant.variant_unit
filteredVariant.variant_unit_scale = variant.variant_unit_scale
hasUpdatableProperty = true
if variant.hasOwnProperty("variant_unit_name")
filteredVariant.variant_unit_name = variant.variant_unit_name
hasUpdatableProperty = true
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}

View File

@@ -4,31 +4,30 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
scope.$watchCollection ->
return [
scope.$eval(attrs.ofnDisplayAs).unit_value_with_description
scope.product.variant_unit_name
scope.product.variant_unit_with_scale
scope.variant.variant_unit_name
scope.variant.variant_unit_with_scale
]
, ->
[variant_unit, variant_unit_scale] = productUnitProperties()
[unit_value, unit_description] = variantUnitProperties(variant_unit_scale)
variant_object =
variant_object =
unit_value: unit_value
unit_description: unit_description
product:
variant_unit_scale: variant_unit_scale
variant_unit: variant_unit
variant_unit_name: scope.product.variant_unit_name
variant_unit_scale: variant_unit_scale
variant_unit: variant_unit
variant_unit_name: scope.variant.variant_unit_name
scope.placeholder_text = new OptionValueNamer(variant_object).name()
productUnitProperties = ->
# get relevant product properties
if scope.product.variant_unit_with_scale?
match = scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if scope.variant.variant_unit_with_scale?
match = scope.variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
variant_unit = match[1]
variant_unit_scale = parseFloat(match[2])
else
variant_unit = scope.product.variant_unit_with_scale
variant_unit = scope.variant.variant_unit_with_scale
variant_unit_scale = null
else
variant_unit = variant_unit_scale = null
@@ -45,4 +44,4 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
unit_value = null if isNaN(unit_value)
unit_value *= variant_unit_scale if unit_value && variant_unit_scale
unit_description = match[3]
[unit_value, unit_description]
[unit_value, unit_description]

View File

@@ -1,8 +0,0 @@
angular.module("ofn.admin").directive "ofnMaintainUnitScale", ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
scope.$watch 'product.variant_unit_with_scale', (newValue, oldValue) ->
if not (oldValue == newValue)
# Triggers track-variant directive to track the unit_value, so that changes to the unit are passed to the server
ngModel.$setViewValue ngModel.$viewValue

View File

@@ -1,8 +0,0 @@
angular.module("ofn.admin").directive "ofnTrackMaster", (DirtyProducts) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty
DirtyProducts.addMasterProperty scope.product.id, scope.product.master.id, attrs.ofnTrackMaster, viewValue
scope.displayDirtyProducts()
viewValue

View File

@@ -199,14 +199,14 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.refreshData()
$scope.getLineItemScale = (lineItem) ->
if lineItem.units_product && lineItem.units_variant && (lineItem.units_product.variant_unit == "weight" || lineItem.units_product.variant_unit == "volume")
lineItem.units_product.variant_unit_scale
if lineItem.units_variant && lineItem.units_variant.variant_unit_scale && (lineItem.units_variant.variant_unit == "weight" || lineItem.units_variant.variant_unit == "volume")
lineItem.units_variant.variant_unit_scale
else
1
$scope.sumUnitValues = ->
sum = $scope.filteredLineItems?.reduce (sum, lineItem) ->
if lineItem.units_product.variant_unit == "items"
if lineItem.units_variant.variant_unit == "items"
sum + lineItem.quantity
else
sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem))
@@ -214,7 +214,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.sumMaxUnitValues = ->
sum = $scope.filteredLineItems?.reduce (sum,lineItem) ->
if lineItem.units_product.variant_unit == "items"
if lineItem.units_variant.variant_unit == "items"
sum + lineItem.max_quantity
else
sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getLineItemScale(lineItem))
@@ -228,39 +228,41 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
return false if !lineItem.hasOwnProperty('final_weight_volume') || !(lineItem.final_weight_volume > 0)
true
$scope.getScale = (unitsProduct, unitsVariant) ->
if unitsProduct.hasOwnProperty("variant_unit") && (unitsProduct.variant_unit == "weight" || unitsProduct.variant_unit == "volume")
unitsProduct.variant_unit_scale
else if unitsProduct.hasOwnProperty("variant_unit") && unitsProduct.variant_unit == "items"
$scope.getScale = (unitsVariant) ->
if unitsVariant.hasOwnProperty("variant_unit") && (unitsVariant.variant_unit == "weight" || unitsVariant.variant_unit == "volume")
unitsVariant.variant_unit_scale
else if unitsVariant.hasOwnProperty("variant_unit") && unitsVariant.variant_unit == "items"
1
else
null
$scope.getFormattedValueWithUnitName = (value, unitsProduct, unitsVariant, scale) ->
unit_name = VariantUnitManager.getUnitName(scale, unitsProduct.variant_unit)
$scope.getFormattedValueWithUnitName = (value, unitsVariant, scale) ->
unit_name = VariantUnitManager.getUnitName(scale, unitsVariant.variant_unit)
$scope.roundToThreeDecimals(value) + " " + unit_name
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
scale = $scope.getScale(unitsProduct, unitsVariant)
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsVariant) ->
scale = $scope.getScale(unitsVariant)
if scale && value
value = value / scale if scale != 28.35 && scale != 1 && scale != 453.6 # divide by scale if not smallest unit
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
else
''
$scope.formattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
scale = $scope.getScale(unitsProduct, unitsVariant)
$scope.formattedValueWithUnitName = (value, unitsVariant) ->
scale = $scope.getScale(unitsVariant)
if scale
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
else
''
$scope.fulfilled = (sumOfUnitValues) ->
# A Units Variant is an API object which holds unit properies of a variant
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size")&& $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
$scope.selectedUnitsProduct.hasOwnProperty("variant_unit")
if $scope.selectedUnitsProduct.variant_unit == "weight" || $scope.selectedUnitsProduct.variant_unit == "volume"
scale = $scope.selectedUnitsProduct.variant_unit_scale
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size") && $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
$scope.selectedUnitsVariant.hasOwnProperty("variant_unit")
if $scope.selectedUnitsVariant.variant_unit == "weight" || $scope.selectedUnitsVariant.variant_unit == "volume"
scale = $scope.selectedUnitsVariant.variant_unit_scale
sumOfUnitValues = sumOfUnitValues * scale unless scale == 28.35 || scale == 453.6
$scope.roundToThreeDecimals(sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size)
else

View File

@@ -1,24 +0,0 @@
angular.module("admin.products").controller "editUnitsCtrl", ($scope, VariantUnitManager) ->
$scope.product =
variant_unit: angular.element('#variant_unit').val()
variant_unit_scale: angular.element('#variant_unit_scale').val()
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
if $scope.product.variant_unit == 'items'
$scope.variant_unit_with_scale = 'items'
else
$scope.variant_unit_with_scale = $scope.product.variant_unit + '_' + $scope.product.variant_unit_scale.replace(/\.0$/, '');
$scope.setFields = ->
if $scope.variant_unit_with_scale == 'items'
variant_unit = 'items'
variant_unit_scale = null
else
options = $scope.variant_unit_with_scale.split('_')
variant_unit = options[0]
variant_unit_scale = options[1]
$scope.product.variant_unit = variant_unit
$scope.product.variant_unit_scale = variant_unit_scale

View File

@@ -1,15 +1,14 @@
# Controller for "New Products" form (spree/admin/products/new)
angular.module("admin.products")
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
$scope.product = { master: {} }
$scope.product.master.product = $scope.product
$scope.product = {}
$scope.placeholder_text = ""
$scope.$watchCollection '[product.variant_unit_with_scale, product.master.unit_value_with_description, product.price, product.variant_unit_name]', ->
$scope.$watchCollection '[product.variant_unit_with_scale, product.unit_value_with_description, product.price, product.variant_unit_name]', ->
$scope.processVariantUnitWithScale()
$scope.processUnitValueWithDescription()
$scope.processUnitPrice()
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name() if $scope.product.variant_unit_scale
$scope.placeholder_text = new OptionValueNamer($scope.product).name() if $scope.product.variant_unit_scale
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
@@ -38,24 +37,24 @@ angular.module("admin.products")
# Extract unit_value and unit_description from text field unit_value_with_description,
# and update hidden variant fields
$scope.processUnitValueWithDescription = ->
if $scope.product.master.hasOwnProperty("unit_value_with_description")
match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
if $scope.product.hasOwnProperty("unit_value_with_description")
match = $scope.product.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
if match
$scope.product.master.unit_value = PriceParser.parse(match[1])
$scope.product.master.unit_value = null if isNaN($scope.product.master.unit_value)
$scope.product.master.unit_value = window.bigDecimal.multiply($scope.product.master.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_description = match[3]
$scope.product.unit_value = PriceParser.parse(match[1])
$scope.product.unit_value = null if isNaN($scope.product.unit_value)
$scope.product.unit_value = window.bigDecimal.multiply($scope.product.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
$scope.product.unit_description = match[3]
else
value = $scope.product.master.unit_value
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description
value = $scope.product.unit_value
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
$scope.product.unit_value_with_description = value + " " + $scope.product.unit_description
# Calculate unit price based on product price and variant_unit_scale
$scope.processUnitPrice = ->
price = $scope.product.price
scale = $scope.product.variant_unit_scale
unit_type = $scope.product.variant_unit
unit_value = $scope.product.master.unit_value
unit_value = $scope.product.unit_value
variant_unit_name = $scope.product.variant_unit_name
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)

View File

@@ -1,32 +0,0 @@
angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices, PriceParser) ->
$scope.unitName = (scale, type) ->
VariantUnitManager.getUnitName(scale, type)
$scope.$watchCollection "[unit_value_human, variant.price]", ->
$scope.processUnitPrice()
$scope.processUnitPrice = ->
if ($scope.variant)
price = $scope.variant.price
scale = $scope.scale
unit_type = angular.element("#product_variant_unit").val()
if (unit_type != "items")
$scope.updateValue()
unit_value = $scope.unit_value
else
unit_value = 1
variant_unit_name = angular.element("#product_variant_unit_name").val()
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)
$scope.scale = angular.element('#product_variant_unit_scale').val()
$scope.updateValue = ->
unit_value_human = angular.element('#unit_value_human').val()
$scope.unit_value = bigDecimal.multiply(PriceParser.parse(unit_value_human), $scope.scale, 2)
variant_unit_value = angular.element('#variant_unit_value').val()
$scope.unit_value_human = parseFloat(bigDecimal.divide(variant_unit_value, $scope.scale, 2))
$timeout -> $scope.processUnitPrice()
$timeout -> $scope.updateValue()

View File

@@ -1,19 +0,0 @@
angular.module("admin.products").directive "setOnDemand", ->
link: (scope, element, attr) ->
onHand = element.context.querySelector("#variant_on_hand")
onDemand = element.context.querySelector("#variant_on_demand")
disableOnHandIfOnDemand = ->
if onDemand.checked
onHand.disabled = 'disabled'
onHand.dataStock = onHand.value
onHand.value = t('admin.products.variants.infinity')
disableOnHandIfOnDemand()
onDemand.addEventListener 'change', (event) ->
disableOnHandIfOnDemand()
if !onDemand.checked
onHand.removeAttribute('disabled')
onHand.value = onHand.dataStock

View File

@@ -13,16 +13,16 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
name_fields.join ' '
value_scaled: ->
@variant.product.variant_unit_scale?
@variant.variant_unit_scale?
option_value_value_unit: ->
if @variant.unit_value?
if @variant.product.variant_unit in ["weight", "volume"]
if @variant.variant_unit in ["weight", "volume"]
[value, unit_name] = @option_value_value_unit_scaled()
else
value = @variant.unit_value
unit_name = @pluralize(@variant.product.variant_unit_name, value)
unit_name = @pluralize(@variant.variant_unit_name, value)
value = parseInt(value, 10) if value == parseInt(value, 10)
@@ -58,14 +58,13 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
# to >= 1 when expressed in it.
# If there is none available where this is true, use the smallest
# available unit.
product = @variant.product
scales = VariantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit)
scales = VariantUnitManager.compatibleUnitScales(@variant.variant_unit_scale, @variant.variant_unit)
variantUnitValue = @variant.unit_value
# sets largestScale = last element in filtered scales array
[_, ..., largestScale] = (scales.filter (s) -> variantUnitValue / s >= 1)
if (largestScale)
[largestScale, VariantUnitManager.getUnitName(largestScale, product.variant_unit)]
[largestScale, VariantUnitManager.getUnitName(largestScale, @variant.variant_unit)]
else
[scales[0], VariantUnitManager.getUnitName(scales[0], product.variant_unit)]
[scales[0], VariantUnitManager.getUnitName(scales[0], @variant.variant_unit)]

View File

@@ -19,7 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
for server_product in serverProducts
product = @findProductInList(server_product.id, @products)
product.variants = server_product.variants
@loadVariantUnitValues product
@loadVariantUnitValues product.variants
find: (id) ->
@findProductInList id, @products
@@ -38,34 +38,32 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
@products.splice(index + 1, 0, newProduct)
unpackProduct: (product) ->
#$scope.matchProducer product
@loadVariantUnit product
loadVariantUnit: (product) ->
product.variant_unit_with_scale =
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
"#{product.variant_unit}_#{product.variant_unit_scale}"
else if product.variant_unit
product.variant_unit
@loadVariantUnitValues product.variants if product.variants
loadVariantUnitValues: (variants) ->
for variant in variants
@loadVariantUnitValue variant
loadVariantUnitValue: (variant) ->
variant.variant_unit_with_scale =
if variant.variant_unit && variant.variant_unit_scale && variant.variant_unit != 'items'
"#{variant.variant_unit}_#{variant.variant_unit_scale}"
else if variant.variant_unit
variant.variant_unit
else
null
@loadVariantUnitValues product if product.variants
@loadVariantUnitValue product, product.master if product.master
loadVariantUnitValues: (product) ->
for variant in product.variants
@loadVariantUnitValue product, variant
loadVariantUnitValue: (product, variant) ->
unit_value = @variantUnitValue product, variant
unit_value = @variantUnitValue variant
unit_value = if unit_value? then unit_value else ''
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
variantUnitValue: (product, variant) ->
variantUnitValue: (variant) ->
if variant.unit_value?
if product.variant_unit_scale
variant_unit_value = @divideAsInteger variant.unit_value, product.variant_unit_scale
if variant.variant_unit_scale
variant_unit_value = @divideAsInteger variant.unit_value, variant.variant_unit_scale
parseFloat(window.bigDecimal.round(variant_unit_value, 2))
else
variant.unit_value

View File

@@ -5,12 +5,7 @@ module Admin
def create
authorize! :admin, enterprise
attributes = {}
attributes[:type] = connected_app_params[:type] if connected_app_params[:type]
app = ConnectedApp.create!(enterprise_id: enterprise.id, **attributes)
app.connect(api_key: spree_current_user.spree_api_key,
channel: SessionChannel.for_request(request))
connect
render_panel
end
@@ -26,6 +21,47 @@ module Admin
private
def create_connected_app
attributes = {}
attributes[:type] = connected_app_params[:type] if connected_app_params[:type]
@app = ConnectedApp.create!(enterprise_id: enterprise.id, **attributes)
end
def connect
return connect_vine if connected_app_params[:type] == "ConnectedApps::Vine"
create_connected_app
@app.connect(api_key: spree_current_user.spree_api_key,
channel: SessionChannel.for_request(request))
end
def connect_vine
if vine_params_empty?
return flash[:error] =
I18n.t("admin.enterprises.form.connected_apps.vine.api_parameters_empty")
end
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)
if !@app.connect(api_key: connected_app_params[:vine_api_key],
secret: connected_app_params[:vine_secret], vine_api:)
error_message = "#{@app.errors.full_messages.to_sentence}. \
#{I18n.t('admin.enterprises.form.connected_apps.vine.api_parameters_error')}".squish
handle_error(error_message)
end
rescue Faraday::Error => e
log_and_notify_exception(e)
handle_error(I18n.t("admin.enterprises.form.connected_apps.vine.connection_error"))
rescue KeyError => e
log_and_notify_exception(e)
handle_error(I18n.t("admin.enterprises.form.connected_apps.vine.setup_error"))
end
def enterprise
@enterprise ||= Enterprise.find(params.require(:enterprise_id))
end
@@ -34,8 +70,22 @@ module Admin
redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel"
end
def handle_error(message)
flash[:error] = message
@app.destroy
end
def log_and_notify_exception(exception)
Rails.logger.error exception.inspect
Bugsnag.notify(exception)
end
def vine_params_empty?
connected_app_params[:vine_api_key].empty? || connected_app_params[:vine_secret].empty?
end
def connected_app_params
params.permit(:type)
params.permit(:type, :vine_api_key, :vine_secret)
end
end
end

View File

@@ -26,10 +26,23 @@ module Admin
# * First step: import all products for given enterprise.
# * Second step: render table and let user decide which ones to import.
imported = graph.map do |subject|
import_product(subject, enterprise)
next unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
existing_variant = enterprise.supplied_variants.linked_to(subject.semanticId)
if existing_variant
SuppliedProductBuilder.update_product(subject, existing_variant)
else
SuppliedProductBuilder.store_product(subject, enterprise)
end
end
@count = imported.compact.count
rescue Faraday::Error,
Addressable::URI::InvalidURIError,
ActionController::ParameterMissing => e
flash[:error] = e.message
redirect_to admin_product_import_path
end
private
@@ -37,18 +50,5 @@ module Admin
def fetch_catalog(url)
DfcRequest.new(spree_current_user).call(url)
end
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.
def import_product(subject, enterprise)
return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
variant = SuppliedProductBuilder.import_variant(subject, enterprise)
product = variant.product
product.save! if product.new_record?
variant.save! if variant.new_record?
variant
end
end
end

View File

@@ -21,8 +21,7 @@ module Admin
@importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user,
params[:settings])
@original_filename = params[:file].try(:original_filename)
@non_updatable_fields = ProductImport::EntryValidator.non_updatable_fields
@non_updatable_fields = ProductImport::EntryValidator.non_updatable_variant_fields
return if contains_errors? @importer
@ams_data = ams_data

View File

@@ -21,7 +21,7 @@ module Api
authorize! :create, Spree::Product
@product = Spree::Product.new(product_params)
if @product.save
if @product.save(context: :create_and_create_standard_variant)
render json: @product, serializer: Api::Admin::ProductSerializer, status: :created
else
invalid_resource!(@product)

View File

@@ -11,6 +11,8 @@ class CartController < BaseController
order.cap_quantity_at_stock!
order.recreate_all_fees!
StockSyncJob.sync_linked_catalogs(order)
render json: { error: false, stock_levels: stock_levels(order) }, status: :ok
else
render json: { error: cart_service.errors.full_messages.join(",") },

View File

@@ -9,6 +9,12 @@ module CheckoutCallbacks
# Otherwise we fail on duplicate indexes or end up with negative stock.
prepend_around_action CurrentOrderLocker, only: [:edit, :update]
# We want to download the latest stock data before anything else happens.
# We don't want it to be in the same database transaction as the order
# locking because this action locks a different set of variants and it
# could cause race conditions.
prepend_around_action :sync_stock, only: :update
prepend_before_action :check_hub_ready_for_checkout
prepend_before_action :check_order_cycle_expiry
prepend_before_action :require_order_cycle
@@ -25,6 +31,14 @@ module CheckoutCallbacks
private
def sync_stock
if current_order&.state == "confirmation"
StockSyncJob.sync_linked_catalogs_now(current_order)
end
yield
end
def load_order
@order = current_order
@order.manual_shipping_selection = true
@@ -63,12 +77,6 @@ module CheckoutCallbacks
end
end
def valid_order_line_items?
@order.insufficient_stock_lines.empty? &&
OrderCycles::DistributedVariantsService.new(@order.order_cycle, @order.distributor).
distributes_order_variants?(@order)
end
def ensure_order_not_completed
redirect_to main_app.cart_path if @order.completed?
end

View File

@@ -51,7 +51,7 @@ module OrderCompletion
def order_invalid!
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
payload.add_metadata :order, @order
payload.add_metadata :order, :order, @order
end
flash[:error] = t('checkout.order_not_loaded')

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ module Spree
def create
delete_stock_params_and_set_after do
@object.attributes = permitted_resource_params
if @object.save
if @object.save(context: :create_and_create_standard_variant)
flash[:success] = flash_message_for(@object, :successfully_created)
redirect_after_save
else
@@ -214,10 +214,10 @@ module Spree
def notify_bugsnag(error, product, variant)
Bugsnag.notify(error) do |report|
report.add_metadata(:product, product.attributes)
report.add_metadata(:product_error, product.errors.first) unless product.valid?
report.add_metadata(:variant, variant.attributes)
report.add_metadata(:variant_error, variant.errors.first) unless variant.valid?
report.add_metadata(:product,
{ product: product.attributes, variant: variant.attributes })
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?
report.add_metadata(:product, :variant_error, variant.errors.first) unless variant.valid?
end
end

View File

@@ -23,7 +23,7 @@ module Spree
def permitted_resource_params
params.require(:return_authorization).
permit(:amount, :reason, :stock_location_id)
permit(:amount, :reason)
end
end
end

View File

@@ -5,6 +5,8 @@ require 'open_food_network/scope_variants_for_search'
module Spree
module Admin
class VariantsController < ::Admin::ResourceController
helper ::Admin::ProductsHelper
belongs_to 'spree/product'
before_action :load_data, only: [:new, :edit]

View File

@@ -18,17 +18,15 @@ module Admin
end
def unit_value_with_description(variant)
precised_unit_value = nil
return variant.unit_description.to_s if variant.unit_value.nil?
if variant.unit_value
scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1)
precised_unit_value = number_with_precision(
scaled_unit_value,
precision: nil,
strip_insignificant_zeros: true,
significant: false,
)
end
scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1)
precised_unit_value = number_with_precision(
scaled_unit_value,
precision: nil,
strip_insignificant_zeros: true,
significant: false,
)
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
end

View File

@@ -58,4 +58,9 @@ module ReportsHelper
.where(order_id: orders.map(&:id))
.pluck(:originator_id)
end
def datepicker_time(datetime)
datetime = Time.zone.parse(datetime) if datetime.is_a? String
datetime.strftime('%Y-%m-%d %H:%M')
end
end

View File

@@ -0,0 +1,98 @@
# frozen_string_literal: true
# When orders are 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 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)
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
end
end

View File

@@ -13,63 +13,73 @@ class BackorderJob < ApplicationJob
sidekiq_options retry: 0
def self.check_stock(order)
variants_needing_stock = order.variants.select do |variant|
# TODO: scope variants to hub.
# We are only supporting producer stock at the moment.
variant.on_hand&.negative?
end
links = SemanticLink.where(subject: order.variants)
linked_variants = variants_needing_stock.select do |variant|
variant.semantic_links.present?
end
perform_later(order, linked_variants) if linked_variants.present?
perform_later(order) if links.exists?
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)
payload.add_metadata(:order, :order, order)
end
end
def perform(order, linked_variants)
def perform(order)
OrderLocker.lock_order_and_variants(order) do
place_backorder(order, linked_variants)
place_backorder(order)
end
rescue StandardError
# If the backordering fails, we need to tell the shop owner because they
# need to organgise more stock.
BackorderMailer.backorder_failed(order, linked_variants).deliver_later
BackorderMailer.backorder_failed(order).deliver_later
raise
end
def place_backorder(order, linked_variants)
def place_backorder(order)
user = order.distributor.owner
items = backorderable_items(order)
return if items.empty?
# We are assuming that all variants are linked to the same wholesale
# shop and its catalog:
urls = FdcUrlBuilder.new(linked_variants[0].semantic_links[0].semantic_id)
reference_link = items[0].variant.semantic_links[0].semantic_id
urls = FdcUrlBuilder.new(reference_link)
orderer = FdcBackorderer.new(user, urls)
backorder = orderer.find_or_build_order(order)
broker = load_broker(order.distributor.owner, urls)
ordered_quantities = {}
linked_variants.each do |variant|
retail_quantity = add_item_to_backorder(variant, broker, backorder, orderer)
ordered_quantities[variant] = retail_quantity
items.each do |item|
retail_quantity = add_item_to_backorder(item, broker, backorder, orderer)
ordered_quantities[item] = retail_quantity
end
place_order(user, order, orderer, backorder)
linked_variants.each do |variant|
variant.on_hand += ordered_quantities[variant]
items.each do |item|
variant = item.variant
variant.on_hand += ordered_quantities[item] if variant.on_demand
end
end
def add_item_to_backorder(variant, broker, backorder, orderer)
needed_quantity = -1 * variant.on_hand
# We look at linked variants which are either stock controlled or
# are on demand with negative stock.
def backorderable_items(order)
order.line_items.select do |item|
# TODO: scope variants to hub.
# We are only supporting producer stock at the moment.
variant = item.variant
variant.semantic_links.present? &&
(variant.on_demand == false || variant.on_hand&.negative?)
end
end
def add_item_to_backorder(line_item, broker, backorder, orderer)
variant = line_item.variant
needed_quantity = needed_quantity(line_item)
solution = broker.best_offer(variant.semantic_links[0].semantic_id)
# The number of wholesale packs we need to order to fulfill the
@@ -88,6 +98,26 @@ class BackorderJob < ApplicationJob
retail_quantity
end
# We have two different types of stock management:
#
# 1. on demand
# We don't restrict sales but account for the quantity sold in our local
# stock level. If it goes negative, we need more stock and trigger a
# backorder.
# 2. limited stock
# The local stock level is a copy from another catalog. We limit sales
# according to that stock level. Every order reduces the local stock level
# and needs to trigger a backorder of the same quantity to stay in sync.
def needed_quantity(line_item)
variant = line_item.variant
if variant.on_demand
-1 * variant.on_hand # on_hand is negative and we need to replenish it.
else
line_item.quantity # We need to order exactly what's we sold.
end
end
def load_broker(user, urls)
FdcOfferBroker.new(user, urls)
end
@@ -103,5 +133,7 @@ class BackorderJob < ApplicationJob
.perform_later(
user, order.distributor, order.order_cycle, placed_order.semanticId
)
order.exchange.semantic_links.create!(semantic_id: placed_order.semanticId)
end
end

View File

@@ -19,12 +19,18 @@ class CompleteBackorderJob < ApplicationJob
# someone else's order.
def perform(user, distributor, order_cycle, order_id)
order = FdcBackorderer.new(user, nil).find_order(order_id)
return if order&.lines.blank?
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
variants = order_cycle.variants_distributed_by(distributor)
adjust_quantities(user, order, urls, variants)
adjust_quantities(order_cycle, user, order, urls, variants)
FdcBackorderer.new(user, urls).complete_order(order)
exchange = order_cycle.exchanges.outgoing.find_by(receiver: distributor)
exchange.semantic_links.find_by(semantic_id: order_id)&.destroy!
rescue StandardError
BackorderMailer.backorder_incomplete(user, distributor, order_cycle, order_id).deliver_later
@@ -36,7 +42,7 @@ class CompleteBackorderJob < ApplicationJob
# 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(user, order, urls, variants)
def adjust_quantities(order_cycle, user, order, urls, variants)
broker = FdcOfferBroker.new(user, urls)
order.lines.each do |line|
@@ -45,18 +51,40 @@ class CompleteBackorderJob < ApplicationJob
transformation = broker.wholesale_to_retail(wholesale_product_id)
linked_variant = variants.linked_to(transformation.retail_product_id)
# Note that a division of integers dismisses the remainder, like `floor`:
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
# 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?
# 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
# 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

@@ -0,0 +1,79 @@
# frozen_string_literal: true
class StockSyncJob < ApplicationJob
# No retry but stay as failed job:
sidekiq_options retry: 0
# We synchronise stock of stock-controlled variants linked to a remote
# product. These variants are rare though and we check first before we
# enqueue a new job. That should save some time loading the order with
# all the stock data to make this decision.
def self.sync_linked_catalogs(order)
user = order.distributor.owner
catalog_ids(order).each do |catalog_id|
perform_later(user, catalog_id)
end
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
end
def self.sync_linked_catalogs_now(order)
user = order.distributor.owner
catalog_ids(order).each do |catalog_id|
perform_now(user, catalog_id)
end
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
end
def self.catalog_ids(order)
stock_controlled_variants = order.variants.reject(&:on_demand)
links = SemanticLink.where(subject: stock_controlled_variants)
semantic_ids = links.pluck(:semantic_id)
semantic_ids.map do |product_id|
FdcUrlBuilder.new(product_id).catalog_url
end.uniq
end
def perform(user, catalog_id)
products = load_products(user, catalog_id)
products_by_id = products.index_by(&:semanticId)
product_ids = products_by_id.keys
variants = linked_variants(user.enterprises, product_ids)
# Avoid race condition between checkout and stock sync.
Spree::Variant.transaction do
variants.order(:id).lock.each do |variant|
next if variant.on_demand
product = products_by_id[variant.semantic_links[0].semantic_id]
catalog_item = product&.catalogItems&.first
CatalogItemBuilder.apply_stock(catalog_item, variant)
variant.stock_items[0].save!
end
end
end
def load_products(user, catalog_id)
json_catalog = DfcRequest.new(user).call(catalog_id)
graph = DfcIo.import(json_catalog)
graph.select do |subject|
subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
end
end
def linked_variants(enterprises, product_ids)
Spree::Variant.where(supplier: enterprises)
.includes(:semantic_links).references(:semantic_links)
.where(semantic_links: { semantic_id: product_ids })
end
end

View File

@@ -56,7 +56,7 @@ class SubscriptionConfirmJob < ApplicationJob
send_failed_payment_email(order)
else
Bugsnag.notify(e) do |payload|
payload.add_metadata :order, order
payload.add_metadata :order, :order, order
end
send_failed_payment_email(order, e.message)
end
@@ -109,8 +109,7 @@ class SubscriptionConfirmJob < ApplicationJob
SubscriptionMailer.failed_payment_email(order).deliver_now
rescue StandardError => e
Bugsnag.notify(e) do |payload|
payload.add_metadata :order, order
payload.add_metadata :error_message, error_message
payload.add_metadata :subscription_data, { order:, error_message: }
end
end
end

View File

@@ -3,9 +3,9 @@
class BackorderMailer < ApplicationMailer
include I18nHelper
def backorder_failed(order, linked_variants)
def backorder_failed(order)
@order = order
@linked_variants = linked_variants
@linked_variants = order.variants
I18n.with_locale valid_locale(order.distributor.owner) do
mail(to: order.distributor.owner.email)

View File

@@ -4,13 +4,14 @@
#
# Here we store keys and links to access the app.
class ConnectedApp < ApplicationRecord
TYPES = ['discover_regen', 'affiliate_sales_data'].freeze
TYPES = ['discover_regen', 'affiliate_sales_data', 'vine'].freeze
belongs_to :enterprise
after_destroy :disconnect
scope :discover_regen, -> { where(type: "ConnectedApp") }
scope :affiliate_sales_data, -> { where(type: "ConnectedApps::AffiliateSalesData") }
scope :vine, -> { where(type: "ConnectedApps::Vine") }
scope :connecting, -> { where(data: nil) }
scope :ready, -> { where.not(data: nil) }

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
# An enterprise can opt-in to use VINE API to manage vouchers
#
module ConnectedApps
class Vine < ConnectedApp
encrypts :data
def connect(api_key:, secret:, vine_api:, **_opts)
response = vine_api.my_team
return update data: { api_key:, secret: } if response.success?
errors.add(:base, I18n.t("activerecord.errors.models.connected_apps.vine.api_request_error"))
false
end
def disconnect; end
end
end

View File

@@ -5,11 +5,6 @@ class CustomTab < ApplicationRecord
validates :title, presence: true, length: { maximum: 20 }
# Remove any unsupported HTML.
def content
HtmlSanitizer.sanitize(super)
end
# Remove any unsupported HTML.
def content=(html)
super(HtmlSanitizer.sanitize(html))

View File

@@ -10,6 +10,8 @@
class Customer < ApplicationRecord
include SetUnusedAddressFields
self.ignored_columns += ['name']
acts_as_taggable
searchable_attributes :first_name, :last_name, :email, :code

View File

@@ -74,11 +74,6 @@ class EnterpriseGroup < ApplicationRecord
permalink
end
# Remove any unsupported HTML.
def long_description
HtmlSanitizer.sanitize_and_enforce_link_target_blank(super)
end
# Remove any unsupported HTML.
def long_description=(html)
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))

View File

@@ -22,6 +22,10 @@ class Exchange < ApplicationRecord
has_many :exchange_fees, dependent: :destroy
has_many :enterprise_fees, through: :exchange_fees
# Links to open backorders of a distributor (outgoing exchanges only)
# Don't allow removal of distributor from OC while we have an open backorder.
has_many :semantic_links, as: :subject, dependent: :restrict_with_error
validates :sender_id, uniqueness: { scope: [:order_cycle_id, :receiver_id, :incoming] }
before_destroy :delete_related_exchange_variants, prepend: true

View File

@@ -224,6 +224,9 @@ module ProductImport
# Ensure attributes are correctly copied to a new product's variant
variant = product.variants.first
variant.display_name = entry.display_name if entry.display_name
variant.variant_unit = entry.variant_unit if entry.variant_unit
variant.variant_unit_name = entry.variant_unit_name if entry.variant_unit_name
variant.variant_unit_scale = entry.variant_unit_scale if entry.variant_unit_scale
variant.import_date = @import_time
variant.supplier_id = entry.producer_id
variant.save

View File

@@ -6,8 +6,6 @@
module ProductImport
class EntryValidator
SKIP_VALIDATE_ON_UPDATE = [:description].freeze
# rubocop:disable Metrics/ParameterLists
def initialize(current_user, import_time, spreadsheet_data, editable_enterprises,
inventory_permissions, reset_counts, import_settings, all_entries)
@@ -22,9 +20,8 @@ module ProductImport
end
# rubocop:enable Metrics/ParameterLists
def self.non_updatable_fields
def self.non_updatable_variant_fields
{
description: :description,
unit_type: :variant_unit_scale,
variant_unit_name: :variant_unit_name,
}
@@ -67,8 +64,7 @@ module ProductImport
def mark_as_new_variant(entry, product_id)
variant_attributes = entry.assignable_attributes.except(
'id', 'product_id', 'on_hand', 'on_demand', 'variant_unit', 'variant_unit_name',
'variant_unit_scale'
'id', 'product_id', 'on_hand', 'on_demand'
)
# Variant needs a product. Product needs to be assigned first in order for
# delegate to work. name= will fail otherwise.
@@ -297,11 +293,11 @@ module ProductImport
end
products.flat_map(&:variants).each do |existing_variant|
unit_scale = existing_variant.product.variant_unit_scale
unit_scale = existing_variant.variant_unit_scale
unscaled_units = entry.unscaled_units.to_f || 0
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
if entry_matches_existing_variant?(entry, existing_variant)
if entry.match_inventory_variant?(existing_variant)
variant_override = create_inventory_item(entry, existing_variant)
return validate_inventory_item(entry, variant_override)
end
@@ -311,17 +307,6 @@ module ProductImport
error: I18n.t('admin.product_import.model.not_found'))
end
def entry_matches_existing_variant?(entry, existing_variant)
display_name_are_the_same?(entry, existing_variant) &&
existing_variant.unit_value == entry.unit_value.to_f
end
def display_name_are_the_same?(entry, existing_variant)
return true if entry.display_name.blank? && existing_variant.display_name.blank?
existing_variant.display_name == entry.display_name
end
def category_validation(entry)
category_name = entry.category
@@ -364,13 +349,13 @@ module ProductImport
return
end
products.each { |product| product_field_errors(entry, product) }
products.flat_map(&:variants).each do |existing_variant|
if entry_matches_existing_variant?(entry, existing_variant) &&
existing_variant.deleted_at.nil?
return mark_as_existing_variant(entry, existing_variant)
end
next unless entry.match_variant?(existing_variant) &&
existing_variant.deleted_at.nil?
variant_field_errors(entry, existing_variant)
return mark_as_existing_variant(entry, existing_variant)
end
mark_as_new_variant(entry, products.first.id)
@@ -392,8 +377,7 @@ module ProductImport
def mark_as_existing_variant(entry, existing_variant)
existing_variant.assign_attributes(
entry.assignable_attributes.except('id', 'product_id', 'variant_unit', 'variant_unit_name',
'variant_unit_scale')
entry.assignable_attributes.except('id', 'product_id')
)
check_on_hand_nil(entry, existing_variant)
@@ -406,11 +390,10 @@ module ProductImport
end
end
def product_field_errors(entry, existing_product)
EntryValidator.non_updatable_fields.each do |display_name, attribute|
next if attributes_match?(attribute, existing_product, entry) ||
attributes_blank?(attribute, existing_product, entry)
next if ignore_when_updating_product?(attribute)
def variant_field_errors(entry, existing_variant)
EntryValidator.non_updatable_variant_fields.each do |display_name, attribute|
next if attributes_match?(attribute, existing_variant, entry) ||
attributes_blank?(attribute, existing_variant, entry)
mark_as_invalid(entry, attribute: display_name,
error: I18n.t('admin.product_import.model.not_updatable'))
@@ -423,10 +406,6 @@ module ProductImport
existing_product_value == convert_to_trusted_type(entry_value, existing_product_value)
end
def ignore_when_updating_product?(attribute)
SKIP_VALIDATE_ON_UPDATE.include? attribute
end
def convert_to_trusted_type(untrusted_attribute, trusted_attribute)
case trusted_attribute
when Integer

View File

@@ -84,6 +84,14 @@ module ProductImport
invalid_attrs.except(* NON_PRODUCT_ATTRIBUTES, *NON_DISPLAY_ATTRIBUTES)
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
private
def remove_empty_skus(attrs)
@@ -99,5 +107,11 @@ module ProductImport
end
end
end
def match_display_name?(variant)
return true if display_name.blank? && variant.display_name.blank?
variant.display_name == display_name
end
end
end

View File

@@ -2,7 +2,9 @@
# Link a Spree::Variant to an external DFC SuppliedProduct.
class SemanticLink < ApplicationRecord
belongs_to :variant, class_name: "Spree::Variant"
self.ignored_columns += [:variant_id]
belongs_to :subject, polymorphic: true
validates :semantic_id, presence: true
end

View File

@@ -35,7 +35,6 @@ module Spree
can [:read, :update, :destroy], Spree::User, id: user.id
can [:index, :read], State
can [:index, :read], StockItem
can [:index, :read], StockLocation
can [:index, :read], StockMovement
can [:index, :read], Taxon
can [:index, :read], Variant
@@ -245,7 +244,7 @@ module Spree
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
:packing, :enterprise_fee_summary, :bulk_coop], :report
:packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report
end
def add_order_cycle_management_abilities(user)

View File

@@ -45,7 +45,8 @@ module Spree
after_destroy :update_order
after_save :update_order
delegate :product, :variant_unit, :unit_description, :display_name, :display_as, to: :variant
delegate :product, :variant_unit, :unit_description, :display_name, :display_as,
:variant_unit_scale, :variant_unit_name, to: :variant
# Allows manual skipping of Stock::AvailabilityValidator
attr_accessor :skip_stock_check, :target_shipment

View File

@@ -67,8 +67,12 @@ module Spree
class_name: 'Spree::Adjustment',
dependent: :destroy
has_many :invoices, dependent: :restrict_with_exception
belongs_to :order_cycle, optional: true
has_one :exchange, ->(order) {
outgoing.to_enterprise(order.distributor)
}, through: :order_cycle, source: :exchanges
has_many :semantic_links, through: :exchange
belongs_to :distributor, class_name: 'Enterprise', optional: true
belongs_to :customer, optional: true
has_one :proxy_order, dependent: :destroy

View File

@@ -142,6 +142,8 @@ 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

@@ -155,7 +155,6 @@ module Spree
if adjustment
adjustment.originator = payment_method
adjustment.label = adjustment_label
adjustment.amount = payment_method.compute_amount(self)
adjustment.save
elsif !processing_refund? && payment_method.present?
payment_method.create_adjustment(adjustment_label, self, true)

View File

@@ -38,6 +38,7 @@ module Spree
# strips all non-price-like characters from the price, taking into account locale settings
def parse_price(price)
return nil if price.blank?
return price unless price.is_a?(String)
separator, _delimiter = I18n.t([:'number.currency.format.separator',

View File

@@ -22,7 +22,12 @@ module Spree
include LogDestroyPerformer
self.belongs_to_required_by_default = false
self.ignored_columns += [:supplier_id]
# These columns have been moved to variant. Currently this is only for documentation purposes,
# because they are declared as attr_accessor below, declaring them as ignored columns has no
# effect
self.ignored_columns += [
:supplier_id, :primary_taxon_id, :variant_unit, :variant_unit_scale, :variant_unit_name
]
acts_as_paranoid
@@ -45,20 +50,30 @@ module Spree
validates_lengths_from_database
validates :name, presence: true
validates :variant_unit, presence: true
validates :unit_value, numericality: {
greater_than: 0,
if: ->(p) { p.variant_unit.in?(%w(weight volume)) && new_record? }
}
validates :variant_unit_scale,
presence: { if: ->(p) { %w(weight volume).include? p.variant_unit } }
validates :variant_unit_name,
presence: { if: ->(p) { p.variant_unit == 'items' } }
validate :validate_image
validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } }
accepts_nested_attributes_for :variants, allow_destroy: true
# These validators are used to make sure the standard variant created via
# `ensure_standard_variant` will be valid. The are only used when creating a new product
with_options on: :create_and_create_standard_variant do
validates :supplier_id, presence: true
validates :primary_taxon_id, presence: true
validates :variant_unit, presence: true
validates :unit_value, presence: true, if: ->(product) {
%w(weight volume).include?(product.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
validates :unit_description, presence: true, if: ->(product) {
product.variant_unit.present? && product.unit_value.nil?
}
validates :variant_unit_scale, presence: true, if: ->(product) {
%w(weight volume).include?(product.variant_unit)
}
validates :variant_unit_name, presence: true, if: ->(product) {
product.variant_unit == 'items'
}
end
accepts_nested_attributes_for :image
accepts_nested_attributes_for :product_properties,
allow_destroy: true,
@@ -66,14 +81,12 @@ module Spree
# Transient attributes used temporarily when creating a new product,
# these values are persisted on the product's variant
attr_accessor :price, :display_as, :unit_value, :unit_description, :tax_category_id,
:shipping_category_id, :primary_taxon_id, :supplier_id
attr_accessor :price, :display_as, :unit_value, :unit_description, :variant_unit,
:variant_unit_name, :variant_unit_scale, :tax_category_id, :shipping_category_id,
:primary_taxon_id, :supplier_id
after_validation :validate_variant_attrs, on: :create
after_create :ensure_standard_variant
after_update :touch_supplier, if: :saved_change_to_primary_taxon_id?
around_destroy :destruction
after_save :update_units
after_touch :touch_supplier
# -- Scopes
@@ -198,10 +211,6 @@ module Spree
end
end
def total_on_hand
stock_items.sum(&:count_on_hand)
end
def properties_including_inherited
# Product properties override producer properties
ps = product_properties.all
@@ -245,6 +254,7 @@ module Spree
end
end
# rubocop:disable Metrics/AbcSize
def ensure_standard_variant
return unless variants.empty?
@@ -254,36 +264,16 @@ module Spree
variant.display_as = display_as
variant.unit_value = unit_value
variant.unit_description = unit_description
variant.variant_unit = variant_unit
variant.variant_unit_name = variant_unit_name
variant.variant_unit_scale = variant_unit_scale
variant.tax_category_id = tax_category_id
variant.shipping_category_id = shipping_category_id
variant.primary_taxon_id = primary_taxon_id
variant.supplier_id = supplier_id
variants << variant
end
# Format as per WeightsAndMeasures (todo: re-orgnaise maybe after product/variant refactor)
def variant_unit_with_scale
# Our code is based upon English based number formatting with a period `.`
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
precision: nil,
significant: false,
strip_insignificant_zeros: true,
locale: :en)
[variant_unit, scale_clean].compact_blank.join("_")
end
def variant_unit_with_scale=(variant_unit_with_scale)
values = variant_unit_with_scale.split("_")
assign_attributes(
variant_unit: values[0],
variant_unit_scale: values[1] || nil
)
end
# Remove any unsupported HTML.
def description
HtmlSanitizer.sanitize(super)
end
# rubocop:enable Metrics/AbcSize
# Remove any unsupported HTML.
def description=(html)
@@ -292,27 +282,6 @@ module Spree
private
def validate_variant_attrs
# Avoid running validation when we can't set variant attrs
# eg clone product. Will raise error if clonning a product with no variant
return if variants.first&.valid?
errors.add(:primary_taxon_id, :blank) unless Spree::Taxon.find_by(id: primary_taxon_id)
errors.add(:supplier_id, :blank) unless Enterprise.find_by(id: supplier_id)
end
def update_units
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
variants.each do |v|
if v.persisted?
v.update_units
else
v.assign_units
end
end
end
def touch_supplier
return if variants.empty?

View File

@@ -2,12 +2,12 @@
module Spree
class ReturnAuthorization < ApplicationRecord
self.ignored_columns += [:stock_location_id]
acts_as_paranoid
belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations
has_many :inventory_units, inverse_of: :return_authorization, dependent: :nullify
has_one :stock_location, dependent: nil
before_save :force_positive_amount
before_create :generate_number

View File

@@ -7,7 +7,7 @@ module Spree
def initialize(variant)
@variant = variant
@stock_items = fetch_stock_items
@stock_items = @variant.stock_items
end
def total_on_hand
@@ -25,16 +25,6 @@ module Spree
def can_supply?(required)
total_on_hand >= required || backorderable?
end
private
def fetch_stock_items
# Don't re-fetch associated stock items from the DB if we've already eager-loaded them
return @variant.stock_items if @variant.stock_items.loaded?
Spree::StockItem.joins(:stock_location).
where(:variant_id => @variant, Spree::StockLocation.table_name => { active: true })
end
end
end
end

View File

@@ -3,7 +3,7 @@
module Spree
class StockLocation < ApplicationRecord
self.belongs_to_required_by_default = false
self.ignored_columns += [:backorderable_default]
self.ignored_columns += [:backorderable_default, :active]
has_many :stock_items, dependent: :delete_all, inverse_of: :stock_location
has_many :stock_movements, through: :stock_items
@@ -13,15 +13,9 @@ module Spree
validates :name, presence: true
scope :active, -> { where(active: true) }
after_create :create_stock_items
# Wrapper for creating a new stock item respecting the backorderable config
def propagate_variant(variant)
stock_items.create!(variant:)
end
def stock_item(variant)
stock_items.where(variant_id: variant).order(:id).first
end
@@ -57,7 +51,7 @@ module Spree
private
def create_stock_items
Variant.find_each { |variant| propagate_variant(variant) }
Variant.find_each { |variant| stock_items.create!(variant:) }
end
end
end

View File

@@ -105,6 +105,15 @@ module Spree
if default_zone_or_zone_match?(item.order)
calculator.compute(item)
else
# Tax refund should not be possible with the way our production server are configured
Bugsnag.notify(
"Notice: Tax refund should not be possible, please check the default zone and " \
"the tax rate zone configuration"
) do |payload|
payload.add_metadata :order_tax_zone, item.order.tax_zone
payload.add_metadata :tax_rate_zone, zone
payload.add_metadata :default_zone, Zone.default_tax
end
# In this case, it's a refund.
calculator.compute(item) * - 1
end

View File

@@ -60,7 +60,7 @@ module Spree
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides, dependent: :destroy
has_many :inventory_items, dependent: :destroy
has_many :semantic_links, dependent: :delete_all
has_many :semantic_links, as: :subject, dependent: :delete_all
has_many :supplier_properties, through: :supplier, source: :properties
localize_number :price, :weight
@@ -71,21 +71,25 @@ module Spree
validates :tax_category, presence: true,
if: proc { Spree::Config.products_require_tax_category }
validates :variant_unit, presence: true
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product&.variant_unit)
%w(weight volume).include?(variant.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
validates :price, numericality: { greater_than_or_equal_to: 0 }
validates :unit_description, presence: true, if: ->(variant) {
variant.product&.variant_unit.present? && variant.unit_value.nil?
variant.variant_unit.present? && variant.unit_value.nil?
}
validates :variant_unit_scale, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.variant_unit)
}
validates :variant_unit_name, presence: true, if: ->(variant) {
variant.variant_unit == 'items'
}
before_validation :set_cost_currency
before_validation :ensure_shipping_category
before_validation :ensure_unit_value
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
before_validation :update_weight_from_unit_value
before_validation :convert_variant_weight_to_decimal
before_save :assign_units, if: ->(variant) {
@@ -95,6 +99,9 @@ module Spree
after_create :create_stock_items
around_destroy :destruction
after_save :save_default_price
after_save :update_units, if: -> {
saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
}
# default variant scope only lists non-deleted variants
scope :deleted, -> { where.not(deleted_at: nil) }
@@ -219,6 +226,25 @@ module Spree
Spree::Stock::Quantifier.new(self).total_on_hand
end
# Format as per WeightsAndMeasures
def variant_unit_with_scale
# Our code is based upon English based number formatting with a period `.`
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
precision: nil,
significant: false,
strip_insignificant_zeros: true,
locale: :en)
[variant_unit, scale_clean].compact_blank.join("_")
end
def variant_unit_with_scale=(variant_unit_with_scale)
values = variant_unit_with_scale.split("_")
assign_attributes(
variant_unit: values[0],
variant_unit_scale: values[1] || nil
)
end
private
def check_currency
@@ -243,12 +269,12 @@ module Spree
return unless stock_items.empty?
StockLocation.find_each do |stock_location|
stock_location.propagate_variant(self)
stock_items.create!(stock_location:)
end
end
def update_weight_from_unit_value
return unless product.variant_unit == 'weight' && unit_value.present?
return unless variant_unit == 'weight' && unit_value.present?
self.weight = weight_from_unit_value
end
@@ -268,7 +294,7 @@ module Spree
def ensure_unit_value
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
return unless (product&.variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
self.unit_value = 1.0
end

View File

@@ -3,8 +3,7 @@
module Api
module Admin
class ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name,
:inherits_properties, :on_hand, :price, :import_date, :image_url,
attributes :id, :name, :sku, :inherits_properties, :on_hand, :price, :import_date, :image_url,
:thumb_url, :variants
def variants

View File

@@ -3,7 +3,7 @@
module Api
module Admin
class UnitsProductSerializer < ActiveModel::Serializer
attributes :id, :name, :group_buy_unit_size, :variant_unit, :variant_unit_scale
attributes :id, :name, :group_buy_unit_size
end
end
end

View File

@@ -3,7 +3,7 @@
module Api
module Admin
class UnitsVariantSerializer < ActiveModel::Serializer
attributes :id, :full_name, :unit_value
attributes :id, :full_name, :unit_value, :variant_unit, :variant_unit_scale
def full_name
full_name = object.full_name

View File

@@ -6,7 +6,8 @@ module Api
attributes :id, :name, :producer_name, :image, :sku, :import_date, :tax_category_id,
:options_text, :unit_value, :unit_description, :unit_to_display,
:display_as, :display_name, :name_to_display, :variant_overrides_count,
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name,
:variant_unit, :variant_unit_scale, :variant_unit_name, :variant_unit_with_scale
has_one :primary_taxon, key: :category_id, embed: :id
has_one :supplier, key: :producer_id, embed: :id

View File

@@ -10,7 +10,7 @@ class FdcBackorderer
end
def find_or_build_order(ofn_order)
find_open_order || build_new_order(ofn_order)
find_open_order(ofn_order) || build_new_order(ofn_order)
end
def build_new_order(ofn_order)
@@ -19,7 +19,37 @@ class FdcBackorderer
end
end
def find_open_order
# Try the new method and fall back to old method.
def find_open_order(ofn_order)
lookup_open_order(ofn_order) || find_last_open_order
end
def lookup_open_order(ofn_order)
# There should be only one link at the moment but we may support
# ordering from multiple suppliers one day.
semantic_ids = ofn_order.semantic_links.pluck(:semantic_id)
semantic_ids.lazy
# Make sure we select an order from the right supplier:
.select { |id| id.starts_with?(urls.orders_url) }
# Fetch the order from the remote DFC server, lazily:
.map { |id| find_order(id) }
.compact
# Just in case someone completed the order without updating our database:
.select { |o| o.orderStatus[:path] == "Held" }
.first
# The DFC Connector doesn't recognise status values properly yet.
# So we are overriding the value with something that can be exported.
&.tap { |o| o.orderStatus = "dfc-v:Held" }
end
# DEPRECATED
#
# We now store links to orders we placed. So we don't need to search
# through all orders and pick a random open one.
# But for compatibility with currently open order cycles that don't have
# a stored link yet, we keep this method as well.
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"

View File

@@ -4,11 +4,10 @@ module PermittedAttributes
class Variant
def self.attributes
[
:id, :sku, :on_hand, :on_demand, :shipping_category_id,
:price, :unit_value, :unit_description,
:display_name, :display_as, :tax_category_id,
:weight, :height, :width, :depth, :taxon_ids, :primary_taxon_id,
:supplier_id
:id, :sku, :on_hand, :on_demand, :shipping_category_id, :price, :unit_value,
:unit_description, :variant_unit, :variant_unit_name, :variant_unit_scale, :display_name,
:display_as, :tax_category_id, :weight, :height, :width, :depth, :taxon_ids,
:primary_taxon_id, :supplier_id
]
end
end

View File

@@ -25,7 +25,7 @@ class PlaceProxyOrder
rescue StandardError => e
summarizer.record_and_log_error(:processing, order, e.message)
Bugsnag.notify(e) do |payload|
payload.add_metadata :order, order
payload.add_metadata :order, :order, order
end
end
@@ -57,8 +57,7 @@ class PlaceProxyOrder
true
rescue StandardError => e
Bugsnag.notify(e) do |payload|
payload.add_metadata :subscription, subscription
payload.add_metadata :proxy_order, proxy_order
payload.add_metadata(:proxy_order, { subscription:, proxy_order: })
end
false
end

View File

@@ -146,11 +146,11 @@ module Sets
def notify_bugsnag(error, product, variant, variant_attributes)
Bugsnag.notify(error) do |report|
report.add_metadata(:product, product.attributes)
report.add_metadata(:product_error, product.errors.first) unless product.valid?
report.add_metadata(:variant_attributes, variant_attributes)
report.add_metadata(:variant, variant.attributes)
report.add_metadata(:variant_error, variant.errors.first) unless variant.valid?
report.add_metadata( :product_set,
{ product: product.attributes, variant_attributes:,
variant: variant.attributes } )
report.add_metadata(:product_set, :product_error, product.errors.first) if !product.valid?
report.add_metadata(:product_set, :variant_error, variant.errors.first) if !variant.valid?
end
end
end

View File

@@ -3,12 +3,11 @@
class UnitPrice
def initialize(variant)
@variant = variant
@product = variant.product
end
def denominator
# catches any case where unit is not kg, lb, or L.
return @variant.unit_value if @product&.variant_unit == "items"
return @variant.unit_value if @variant.variant_unit == "items"
case unit
when "lb"
@@ -23,13 +22,13 @@ class UnitPrice
def unit
return "lb" if WeightsAndMeasures.new(@variant).system == "imperial"
case @product&.variant_unit
case @variant.variant_unit
when "weight"
"kg"
when "volume"
"L"
else
@product.variant_unit_name.presence || I18n.t("item")
@variant.variant_unit_name.presence || I18n.t("item")
end
end
end

View File

@@ -32,16 +32,18 @@ module VariantUnits
private
def value_scaled?
@nameable.product.variant_unit_scale.present?
@nameable.variant_unit_scale.present?
end
def option_value_value_unit
if @nameable.unit_value.present? && @nameable.product&.persisted?
if %w(weight volume).include? @nameable.product.variant_unit
if @nameable.unit_value.present?
if %w(weight volume).include? @nameable.variant_unit
value, unit_name = option_value_value_unit_scaled
else
value = @nameable.unit_value
unit_name = pluralize(@nameable.product.variant_unit_name, value)
unit_name = @nameable.variant_unit_name
unit_name = pluralize(unit_name, value) if unit_name.present?
end
value = value.to_i if value == value.to_i

View File

@@ -64,12 +64,12 @@ module VariantUnits
def unit_value_attributes
units = { unit_presentation: option_value_name }
units.merge!(variant_unit: product.variant_unit) if has_attribute?(:variant_unit)
units.merge!(variant_unit:) if has_attribute?(:variant_unit)
units
end
def weight_from_unit_value
(unit_value || 0) / 1000 if product.variant_unit == 'weight'
(unit_value || 0) / 1000 if variant_unit == 'weight'
end
private

View File

@@ -0,0 +1,39 @@
# 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

@@ -0,0 +1,21 @@
# 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

@@ -16,10 +16,10 @@ class WeightsAndMeasures
def system
return "custom" unless scales = scales_for_variant_unit(ignore_available_units: true)
product_scale = @variant.product.variant_unit_scale&.to_f
return "custom" unless product_scale.present? && product_scale.positive?
variant_scale = @variant.variant_unit_scale&.to_f
return "custom" unless variant_scale.present? && variant_scale.positive?
scales[product_scale]['system']
scales[variant_scale]['system']
end
# @returns enumerable with label and value for select
@@ -92,9 +92,9 @@ class WeightsAndMeasures
}.freeze
def scales_for_variant_unit(ignore_available_units: false)
return @units[@variant.product.variant_unit] if ignore_available_units
return @units[@variant.variant_unit] if ignore_available_units
@units[@variant.product.variant_unit]&.reject { |_scale, unit_info|
@units[@variant.variant_unit]&.reject { |_scale, unit_info|
self.class.available_units.exclude?(unit_info['name'])
}
end

View File

@@ -0,0 +1,30 @@
%section.connected_app
.connected-app__head
%div
%h3= t ".title"
%p= t ".tagline"
.connected-app__vine
- if connected_app.nil?
= form_with url: admin_enterprise_connected_apps_path(enterprise.id) do |f|
.connected-app__vine-content
.vine-api-key
= f.hidden_field :type, value: "ConnectedApps::Vine"
= f.label :vine_api_key, t(".vine_api_key")
%span.required *
= f.text_field :vine_api_key, { disabled: !managed_by_user?(enterprise) }
= f.label :vine_secret, t(".vine_secret")
%span.required *
= f.text_field :vine_secret, { disabled: !managed_by_user?(enterprise) }
%div
- disabled = managed_by_user?(enterprise) ? {} : { disabled: true, "data-disable-with": false }
= f.submit t(".enable"), disabled
-# This is only seen by super-admins:
%em= t(".need_to_be_manager") unless managed_by_user?(enterprise)
- else
.connected-app__vine-content
.vine-disable
= button_to t(".disable"), admin_enterprise_connected_app_path(connected_app.id, enterprise_id: enterprise.id), method: :delete
%hr
.connected-app__description
= t ".description_html"

View File

@@ -7,18 +7,8 @@
%td.col-sku.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on product, :sku
%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.hidden_field :variant_unit
= f.hidden_field :variant_unit_scale
= f.select :variant_unit_with_scale,
options_for_select(WeightsAndMeasures.variant_unit_options, product.variant_unit_with_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"}
.field
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (product.variant_unit == "items" ? "" : "display: none")
= error_message_on product, :variant_unit_name, 'data-toggle-control-target': 'control'
%td.col-unit_scale.align-right
-# empty
%td.col-unit.align-right
-# empty
%td.col-price.align-right

View File

@@ -47,7 +47,7 @@
.form-buttons
%a.button.reset.medium{ href: admin_products_path(page: @page, per_page: @per_page, search_term: @search_term, producer_id: @producer_id, category_id: @category_id), 'data-turbo': "false" }
= t('.reset')
= form.submit t('.save'), class: "medium"
= form.submit t('.save'), { class: "medium", data: { action: "click->bulk-form#popoutEmptyVariantUnit" }}
%tr
%th.col-image.align-left= # image
= render partial: 'spree/admin/shared/stimulus_sortable_header',

View File

@@ -7,8 +7,17 @@
%td.col-sku.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on variant, :sku
%td.col-unit_scale
-# empty
%td.col-unir_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.hidden_field :variant_unit
= 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 },
{ 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
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (variant.variant_unit == "items" ? "" : "display: none")
= error_message_on variant, :variant_unit_name, 'data-toggle-control-target': 'control'
%td.col-unit.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do
= variant.unit_to_display # Show the generated summary of unit values
@@ -18,7 +27,7 @@
= f.hidden_field :unit_value
= f.hidden_field :unit_description
= f.text_field :unit_value_with_description,
value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value')
value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value'), required: true
.field
= f.label :display_as, t('admin.products_page.columns.display_as')
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name

View File

@@ -11,7 +11,7 @@
= render partial: 'spree/admin/shared/product_sub_menu'
#products_v3_page{ "data-controller": "products", 'data-turbo': true }
#products_v3_page{ 'data-turbo': true }
= render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term,
producer_options: producers, producer_id: @producer_id,
category_options: categories, category_id: @category_id,

View File

@@ -59,18 +59,18 @@
.variant-unit
= variant.unit_to_display
.small-4.medium-3.columns.variant-price
= number_to_currency(variant.price)
= Spree::Money.new(variant.price)
.unit-price.variant-unit-price
= render AdminTooltipComponent.new(text: t("js.shopfront.unit_price_tooltip"), link_text: "", placement: "top", link_class: "question-mark-icon")
- # TODO use an helper
- unit_price = UnitPrice.new(variant)
- price_per_unit = variant.price / (unit_price.denominator || 1)
= "#{number_to_currency(price_per_unit)}&nbsp;/&nbsp;#{unit_price.unit}".html_safe
= "#{Spree::Money.new(price_per_unit)}&nbsp;/&nbsp;#{unit_price.unit}".html_safe
.medium-3.columns.total-price
%span
= number_to_currency(0.00)
= Spree::Money.new(0.00)
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right
.variant-quantity-inputs
%button.add-variant

View File

@@ -1,14 +1,17 @@
-# Field used for ransack search. This date range is mostly used for Spree::Order
-# so default field is 'completed_at'
- field ||= 'completed_at'
- start_date ||= params[:q].try(:[], :completed_at_gt)
- end_date ||= params[:q].try(:[], :completed_at_lt)
- start_field = "#{field}_gt"
- end_field = "#{field}_lt"
- query = params[:q].to_h
- start_date = datepicker_time(query[start_field].presence || 3.months.ago.beginning_of_day)
- end_date = datepicker_time(query[end_field].presence || Time.zone.tomorrow.beginning_of_day)
.row.date-range-filter
.alpha.two.columns= label_tag nil, t(:date_range)
.omega.fourteen.columns
.field-block.omega.four.columns
.date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range", "flatpickr-enable-time-value": true , "flatpickr-default-hour": 0 } }
.date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range", "flatpickr-enable-time-value": true , "flatpickr-default-hour": 0, "flatpickr-default-date": [start_date, end_date] } }
= text_field_tag nil, nil, class: "datepicker fullwidth", data: { "flatpickr-target": "instance", action: "flatpickr_clear@window->flatpickr#clear" }
= text_field_tag "q[#{field}_gt]", nil, data: { "flatpickr-target": "start" }, style: "display: none", value: start_date
= text_field_tag "q[#{field}_lt]", nil, data: { "flatpickr-target": "end" }, style: "display: none", value: end_date
= text_field_tag "q[#{start_field}]", nil, data: { "flatpickr-target": "start" }, style: "display: none", value: start_date
= text_field_tag "q[#{end_field}]", nil, data: { "flatpickr-target": "end" }, style: "display: none", value: end_date

View File

@@ -1,5 +1,5 @@
= render partial: 'admin/reports/date_range_form',
locals: { f: f, field: 'order_completed_at', start_date: params[:q].try(:[], :order_completed_at_gt), end_date: params[:q].try(:[], :order_completed_at_lt) }
locals: { f: f, field: 'order_completed_at' }
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
@@ -14,4 +14,4 @@
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= select_tag("q[order_cycle_id_in]", options_for_select(report_order_cycle_options(@data.order_cycles), params.dig(:q, :order_cycle_id_in)), {class: "select2 fullwidth", multiple: true})
= select_tag("q[order_cycle_id_in]", options_for_select(report_order_cycle_options(@data.order_cycles), params.dig(:q, :order_cycle_id_in)), {class: "select2 fullwidth", multiple: true})

View File

@@ -0,0 +1,14 @@
= render 'admin/reports/date_range_form', f: f
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.orders_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_producers)
.omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true})
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})

View File

@@ -80,15 +80,15 @@
.three.columns
.text-center
= t("admin.orders.bulk_management.group_buy_unit_size")
.text-center {{ getGroupBySizeFormattedValueWithUnitName(selectedUnitsProduct.group_buy_unit_size , selectedUnitsProduct, selectedUnitsVariant ) }}
.text-center {{ getGroupBySizeFormattedValueWithUnitName(selectedUnitsProduct.group_buy_unit_size , selectedUnitsVariant ) }}
.three.columns
.text-center
= t("admin.orders.bulk_management.total_qtt_ordered")
.text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }}
.text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsVariant ) }}
.three.columns
.text-center
= t("admin.orders.bulk_management.max_qtt_ordered")
.text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }}
.text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsVariant ) }}
.three.columns
.text-center
= t("admin.orders.bulk_management.current_fulfilled_units")

View File

@@ -1,5 +1,5 @@
%div.admin-product-form-fields
.left.twelve.columns.alpha
.left.sixteen.columns.alpha
= f.field_container :name do
= f.label :name, raw(t(:name) + content_tag(:span, ' *', :class => 'required'))
= f.text_field :name, :class => 'fullwidth title'
@@ -10,25 +10,6 @@
= f.hidden_field :description, id: "product_description", value: @product.description
%trix-editor{ input: "product_description", "data-controller": "trixeditor" }
.right.four.columns.omega
.variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' }
= f.field_container :units do
= f.label :variant_unit_with_scale, t(:spree_admin_variant_unit_scale)
%select.select2.fullwidth{ id: 'product_variant_unit_with_scale', 'ng-model' => 'variant_unit_with_scale', 'ng-change' => 'setFields()', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
%option{'value' => ''}
= f.text_field :variant_unit, {'id' => 'variant_unit', 'ng-value' => 'product.variant_unit', 'hidden' => true}
= f.text_field :variant_unit_scale, {'id' => 'variant_unit_scale', 'ng-value' => 'product.variant_unit_scale', 'hidden' => true}
.variant_unit_name{'ng-show' => 'product.variant_unit == "items"'}
= f.field_container :variant_unit_name do
= f.label :variant_unit_name, t(:spree_admin_variant_unit_name)
= f.text_field :variant_unit_name, {placeholder: t('admin.products.unit_name_placeholder')}
= f.error_message_on :variant_unit_name
.clear
.clear
%div

View File

@@ -11,9 +11,6 @@
%td.name{ 'ng-show' => 'columns.name.visible' }
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
%td.unit{ 'ng-show' => 'columns.unit.visible' }
%select.no-search{ "data-controller": "tom-select", 'ng-model' => 'product.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-product' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
%input{ 'ng-model' => 'product.master.unit_value_with_description', :name => 'master_unit_value_with_description', 'ofn-track-master' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "!hasVariants(product) && hasUnit(product)", 'ofn-maintain-unit-scale' => true }
%input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' }
%td.display_as{ 'ng-show' => 'columns.unit.visible' }
%td.price{ 'ng-show' => 'columns.price.visible' }
%input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' }

View File

@@ -10,7 +10,9 @@
%td{ 'ng-show' => 'columns.name.visible' }
%input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" }
%td.unit_value{ 'ng-show' => 'columns.unit.visible' }
%input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', 'ofn-maintain-unit-scale' => true }
%select.no-search{ "data-controller": "tom-select", 'ng-model' => 'variant.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-variant' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
%input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "hasUnit(variant)" }
%input{ 'ng-model' => 'variant.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-variant' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "variant.variant_unit_with_scale == 'items'", :type => 'text' }
%td.display_as{ 'ng-show' => 'columns.unit.visible' }
%input{ 'ofn-display-as' => 'variant', 'ng-model' => 'variant.display_as', name: 'variant_display_as', 'ofn-track-variant' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}' }
%td{ 'ng-show' => 'columns.price.visible' }

View File

@@ -48,9 +48,9 @@
= f.field_container :unit_value do
= f.label :unit_value, t(".value"), 'ng-disabled' => "!hasUnit(product)"
%span.required *
= f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.master.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)"
%input{ type: 'hidden', 'ng-value': 'product.master.unit_value', "ng-init": "product.master.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' }
%input{ type: 'hidden', 'ng-value': 'product.master.unit_description', "ng-init": "product.master.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' }
= f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)"
%input{ type: 'hidden', 'ng-value': 'product.unit_value', "ng-init": "product.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' }
%input{ type: 'hidden', 'ng-value': 'product.unit_description', "ng-init": "product.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' }
= f.error_message_on :unit_value
= render 'display_as', f: f
.six.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" }

View File

@@ -41,8 +41,3 @@
= f.label :reason, t('.reason')
= f.text_area :reason, { style: 'height:100px;', class: 'fullwidth' }
= f.error_message_on :reason
= f.field_container :stock_location do
= f.label :stock_location, t('.stock_location')
= f.select :stock_location_id, Spree::StockLocation.active.all.collect{ |l| [l.name, l.id] }, { style: 'height:100px;', class: 'fullwidth' }
= f.error_message_on :reason

View File

@@ -1,83 +1,112 @@
.label-block.left.six.columns.alpha{'ng-app' => 'admin.products', 'ng-controller' => 'variantUnitsCtrl'}
.field
= f.label :display_name, t('.display_name')
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
.field
= f.label :display_as, t('.display_as')
= f.text_field :display_as, class: "fullwidth", placeholder: t('.display_as_placeholder')
%div{'data-controller': "edit-variant", id: "edit_variant"}
.label-block.left.six.columns.alpha
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted }
- if @product.variant_unit != 'items'
.field
= label_tag :unit_value_human, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})"
= hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale
= number_field_tag :unit_value_human, nil, {class: "fullwidth", step: 0.01, 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'}
= f.number_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'}
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) }
.field
= f.label :unit_description, t(:spree_admin_unit_description)
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
.field
= f.label :display_name, t('.display_name')
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
%div
.field
= f.label :sku, t('.sku')
= f.text_field :sku, class: 'fullwidth'
.field
= f.label :price, t('.price')
= f.text_field :price, class: 'fullwidth', "ng-model" => "variant.price", "ng-init" => "variant.price = '#{number_to_currency(@variant.price, unit: '')&.strip}'"
.field
= hidden_field_tag 'product_variant_unit', @product.variant_unit
= hidden_field_tag 'product_variant_unit_name', @product.variant_unit_name
= f.field_container :unit_price do
%div{style: "display: flex"}
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.admin.unit_price_tooltip'"}
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]",
"class" => 'fullwidth', "disabled" => true, "ng-model" => "unit_price"}
%div{style: "color: black"}
= t("spree.admin.products.new.unit_price_legend")
%div{ 'set-on-demand' => '' }
.field.checkbox
%label
= f.check_box :on_demand
= t(:on_demand)
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
%a= t('admin.whats_this')
.field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.label :unit_scale do
= t('.unit_scale')
= content_tag(:span, ' *', class: 'required')
= f.hidden_field :variant_unit
= 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 },
{ class: "fullwidth no-input", 'aria-label': t('.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } }
= error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control'
.field
= f.label :on_hand, t(:on_hand)
.fullwidth
= f.text_field :on_hand
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (@variant.variant_unit == "items" ? "" : "display: none")
= error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control'
.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
= f.label :unit do
= t('.unit')
= content_tag(:span, ' *', class: 'required')
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('.unit'), 'data-popout-target': "button" do
= @variant.unit_to_display # Show the generated summary of unit values
%div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
.field
-# Show a composite field for unit_value and unit_description
= f.hidden_field :unit_value
= f.hidden_field :unit_description
= f.text_field :unit_value_with_description,
value: unit_value_with_description(@variant), 'aria-label': t('.unit_value'), required: true
.field
= f.label :display_as, t('.display_as')
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(@variant).name
= error_message_on @variant, :unit_value
.right.six.columns.omega.label-block
- if @product.variant_unit != 'weight'
%div
.field
= f.label :sku, t('.sku')
= f.text_field :sku, class: 'fullwidth'
.field
= f.label :price do
= t('.price')
= content_tag(:span, ' *', class: 'required')
= f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip
.field
= hidden_field_tag 'variant_variant_unit', @variant.variant_unit
= hidden_field_tag 'variant_variant_unit_name', @variant.variant_unit_name
= f.field_container :unit_price do
%div{style: "display: flex"}
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.admin.unit_price_tooltip'"}
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", "class" => 'fullwidth', "disabled" => true}
%div{style: "color: black"}
= t("spree.admin.products.new.unit_price_legend")
%div
.field.checkbox
%label
= f.check_box :on_demand, data: { "action": "click->edit-variant#toggleOnHand" }
= t(:on_demand)
= render AdminTooltipComponent.new(text: t('admin.products.variants.to_order_tip'), link_text: t('admin.whats_this'), placement: "right")
.field
= f.label :on_hand, t(:on_hand)
.fullwidth
= f.text_field :on_hand, data: { "edit-variant-target": "onHand" }
.right.six.columns.omega.label-block
.field
= f.label 'weight', t(:weight)+' (kg)'
- value = number_with_precision(@variant.weight, precision: 3)
= f.number_field 'weight', value: value, class: 'fullwidth', step: 0.001
- [:height, :width, :depth].each do |field|
- [:height, :width, :depth].each do |field|
.field
= f.label field, t(field)
- value = number_with_precision(@variant.send(field), precision: 2)
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
.field
= f.label field, t(field)
- value = number_with_precision(@variant.send(field), precision: 2)
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
= f.label :tax_category, t(:tax_category), for: :tax_category_id
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
.field
= f.label :tax_category, t(:tax_category), for: :tax_category_id
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
.field
= f.label :shipping_category_id, t(:shipping_categories)
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
.field
= f.label :shipping_category_id, t(:shipping_categories)
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
.field
= f.label :primary_taxon, t('.variant_category')
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
.field
= f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category')
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
.field
= f.label :supplier do
= t(:spree_admin_supplier)
= content_tag(:span, ' *', class: 'required')
.field
= f.label :supplier, t(:spree_admin_supplier)
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
.clear
.clear

View File

@@ -93,6 +93,16 @@ export default class BulkFormController extends Controller {
}
}
// Pop out empty variant unit to allow browser side validation to focus the element
popoutEmptyVariantUnit() {
this.variantUnits = this.element.querySelectorAll("button.popout__button");
this.variantUnits.forEach((element) => {
if (element.textContent == "") {
element.click();
}
});
}
// private
#registerSubmit() {
@@ -135,7 +145,7 @@ export default class BulkFormController extends Controller {
// Check if changed, and mark with class if it is.
#checkIsChanged(element) {
if(!element.isConnected) return false;
if (!element.isConnected) return false;
const changed = this.#isChanged(element);
element.classList.toggle("changed", changed);
@@ -143,9 +153,8 @@ export default class BulkFormController extends Controller {
}
#isChanged(element) {
if (element.type == "checkbox") {
if (element.type == "checkbox") {
return element.defaultChecked !== undefined && element.checked != element.defaultChecked;
} else if (element.type == "select-one") {
// (weird) Behavior of select element's include_blank option in Rails:
// If a select field has include_blank option selected (its value will be ''),
@@ -155,42 +164,49 @@ export default class BulkFormController extends Controller {
opt.hasAttribute("selected"),
);
const selectedOption = element.selectedOptions[0];
const areBothBlank = selectedOption.value === '' && defaultSelected === undefined
const areBothBlank = selectedOption.value === "" && defaultSelected === undefined;
return !areBothBlank && selectedOption !== defaultSelected;
} else {
return element.defaultValue !== undefined && element.value != element.defaultValue;
}
}
#removeAnimationClasses(productRowElement) {
productRowElement.classList.remove('slide-in');
productRowElement.removeEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement));
productRowElement.classList.remove("slide-in");
productRowElement.removeEventListener(
"animationend",
this.#removeAnimationClasses.bind(this, productRowElement),
);
}
#observeProductsTableRows() {
this.productsTableObserver = new MutationObserver((mutationList, _observer) => {
const mutationRecord = mutationList[0];
if(mutationRecord) {
if (mutationRecord) {
// Right now we are only using it for product clone, so it's always first
const productRowElement = mutationRecord.addedNodes[0];
if (productRowElement) {
productRowElement.addEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement));
productRowElement.addEventListener(
"animationend",
this.#removeAnimationClasses.bind(this, productRowElement),
);
// This is equivalent to form.elements.
const productRowFormElements = productRowElement.querySelectorAll('input, select, textarea, button');
const productRowFormElements = productRowElement.querySelectorAll(
"input, select, textarea, button",
);
this.#registerElements(productRowFormElements);
this.toggleFormChanged();
}
}
});
const productsTable = document.querySelector('.products');
const productsTable = document.querySelector(".products");
// Above mutation function will trigger,
// whenever +products+ table rows (first level children) are mutated i.e. added or removed
// right now we are using this for product clone
// right now we are using this for product clone
this.productsTableObserver.observe(productsTable, { childList: true });
}
}

View File

@@ -0,0 +1,189 @@
import { Controller } from "stimulus";
import OptionValueNamer from "js/services/option_value_namer";
import UnitPrices from "js/services/unit_prices";
// Dynamically update related variant fields
//
// TODO refactor so we can extract what's common with Bulk product page
export default class EditVariantController extends Controller {
static targets = ["onHand"];
connect() {
this.unitPrices = new UnitPrices();
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
// It could automatically find (and cache a ref to) each dom element and get/set the values.
this.variantUnit = this.element.querySelector('[id="variant_variant_unit"]');
this.variantUnitScale = this.element.querySelector('[id="variant_variant_unit_scale"]');
this.variantUnitName = this.element.querySelector('[id="variant_variant_unit_name"]');
this.variantUnitWithScale = this.element.querySelector(
'[id="variant_variant_unit_with_scale"]',
);
this.variantPrice = this.element.querySelector('[id="variant_price"]');
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
passive: true,
});
this.unitValue = this.element.querySelector('[id="variant_unit_value"]');
this.unitDescription = this.element.querySelector('[id="variant_unit_description"]');
this.unitValueWithDescription = this.element.querySelector(
'[id="variant_unit_value_with_description"]',
);
this.displayAs = this.element.querySelector('[id="variant_display_as"]');
this.unitToDisplay = this.element.querySelector('[id="variant_unit_to_display"]');
// on unit changed; update display_as:placeholder and unit_to_display
[this.variantUnit, this.variantUnitScale, this.variantUnitName].forEach((element) => {
element.addEventListener("change", this.#unitChanged.bind(this), { passive: true });
});
this.variantUnitName.addEventListener("input", this.#unitChanged.bind(this), { passive: true });
// on unit_value_with_description changed; update unit_value and unit_description
// on unit_value and/or unit_description changed; update display_as:placeholder and unit_to_display
this.unitValueWithDescription.addEventListener("input", this.#unitChanged.bind(this), {
passive: true,
});
// on display_as changed; update unit_to_display
// TODO: optimise to avoid unnecessary OptionValueNamer calc
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
// update Unit price when variant_unit_with_scale or price changes
[this.variantUnitWithScale, this.variantPrice].forEach((element) => {
element.addEventListener("change", this.#processUnitPrice.bind(this), { passive: true });
});
this.unitValueWithDescription.addEventListener("input", this.#processUnitPrice.bind(this), {
passive: true,
});
// on variantUnit change we need to check if weight needs to be toggled
this.variantUnit.addEventListener("change", this.#toggleWeight.bind(this), { passive: true });
// make sure the unit is correct when page is reload after an error
this.#updateUnitDisplay();
// update unit price on page load
this.#processUnitPrice();
if (this.variantUnit.value === "weight") {
return this.#hideWeight();
}
}
disconnect() {
// Make sure to clean up anything that happened outside
// TODO remove all added event
this.variantUnit.removeEventListener("change", this.#toggleWeight.bind(this), {
passive: true,
});
}
toggleOnHand(event) {
if (event.target.checked === true) {
this.onHandTarget.dataStock = this.onHandTarget.value;
this.onHandTarget.value = I18n.t("admin.products.variants.infinity");
this.onHandTarget.disabled = "disabled";
} else {
this.onHandTarget.removeAttribute("disabled");
this.onHandTarget.value = this.onHandTarget.dataStock;
}
}
// private
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#unitChanged(event) {
//Hmm in hindsight the logic in product_controller should be inn this controller already. then we can do everything in one event, and store the generated name in an instance variable.
this.#extractUnitValues();
this.#updateUnitDisplay();
}
// Extract unit_value and unit_description
#extractUnitValues() {
// Extract a number (optional) and text value, separated by a space.
const match = this.unitValueWithDescription.value.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/);
if (match) {
let unit_value = parseFloat(match[1].replace(",", "."));
unit_value = isNaN(unit_value) ? null : unit_value;
unit_value *= this.variantUnitScale.value ? this.variantUnitScale.value : 1; // Normalise to default scale
this.unitValue.value = unit_value;
this.unitDescription.value = match[3];
}
}
// Update display_as placeholder and unit_to_display
#updateUnitDisplay() {
const unitDisplay = new OptionValueNamer(this.#variant()).name();
this.displayAs.placeholder = unitDisplay;
this.unitToDisplay.textContent = this.displayAs.value || unitDisplay;
}
// A representation of the variant model to satisfy OptionValueNamer.
#variant() {
return {
unit_value: parseFloat(this.unitValue.value),
unit_description: this.unitDescription.value,
variant_unit: this.variantUnit.value,
variant_unit_scale: parseFloat(this.variantUnitScale.value),
variant_unit_name: this.variantUnitName.value,
};
}
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#updateUnitAndScale(event) {
const variant_unit_with_scale = this.variantUnitWithScale.value;
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
if (match) {
this.variantUnit.value = match[1];
this.variantUnitScale.value = parseFloat(match[2]);
} else {
// "items"
this.variantUnit.value = variant_unit_with_scale;
this.variantUnitScale.value = "";
}
this.variantUnit.dispatchEvent(new Event("change"));
this.variantUnitScale.dispatchEvent(new Event("change"));
}
#processUnitPrice() {
const unit_type = this.variantUnit.value;
// TODO double check this
let unit_value = 1;
if (unit_type != "items") {
unit_value = this.unitValue.value;
}
const unit_price = this.unitPrices.displayableUnitPrice(
this.variantPrice.value,
parseFloat(this.variantUnitScale.value),
unit_type,
unit_value,
this.variantUnitName.value,
);
this.element.querySelector('[id="variant_unit_price"]').value = unit_price;
}
#hideWeight() {
this.weight = this.element.querySelector('[id="variant_weight"]');
this.weight.parentElement.style.display = "none";
}
#toggleWeight() {
if (this.variantUnit.value === "weight") {
return this.#hideWeight();
}
// Show weight
this.weight = this.element.querySelector('[id="variant_weight"]');
this.weight.parentElement.style.display = "block";
// Clearing weight value to remove calculated weight for a variant with unit set to "weight"
// See Spree::Variant hook update_weight_from_unit_value
this.weight.value = "";
}
}

View File

@@ -1,38 +0,0 @@
import { Controller } from "stimulus";
// Dynamically update related Product unit fields (expected to move to Variant due to Product Refactor)
//
export default class ProductController extends Controller {
connect() {
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
// It could automatically find (and cache a ref to) each dom element and get/set the values.
this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]');
this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]');
this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]');
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
passive: true,
});
}
// private
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#updateUnitAndScale(event) {
const variant_unit_with_scale = this.variantUnitWithScale.value;
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
if (match) {
this.variantUnit.value = match[1];
this.variantUnitScale.value = parseFloat(match[2]);
} else {
// "items"
this.variantUnit.value = variant_unit_with_scale;
this.variantUnitScale.value = "";
}
this.variantUnit.dispatchEvent(new Event("change"));
this.variantUnitScale.dispatchEvent(new Event("change"));
}
}

View File

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

View File

@@ -5,11 +5,17 @@ import OptionValueNamer from "js/services/option_value_namer";
//
export default class VariantController extends Controller {
connect() {
// Assuming these will be available on the variant soon, just a quick hack to find the product fields:
const product = this.element.closest("[data-record-id]");
this.variantUnit = product.querySelector('[name$="[variant_unit]"]');
this.variantUnitScale = product.querySelector('[name$="[variant_unit_scale]"]');
this.variantUnitName = product.querySelector('[name$="[variant_unit_name]"]');
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
// It could automatically find (and cache a ref to) each dom element and get/set the values.
this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]');
this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]');
this.variantUnitName = this.element.querySelector('[name$="[variant_unit_name]"]');
this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]');
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
passive: true,
});
this.unitValue = this.element.querySelector('[name$="[unit_value]"]');
this.unitDescription = this.element.querySelector('[name$="[unit_description]"]');
@@ -76,11 +82,27 @@ export default class VariantController extends Controller {
return {
unit_value: parseFloat(this.unitValue.value),
unit_description: this.unitDescription.value,
product: {
variant_unit: this.variantUnit.value,
variant_unit_scale: parseFloat(this.variantUnitScale.value),
variant_unit_name: this.variantUnitName.value,
},
variant_unit: this.variantUnit.value,
variant_unit_scale: parseFloat(this.variantUnitScale.value),
variant_unit_name: this.variantUnitName.value,
};
}
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#updateUnitAndScale(event) {
const variant_unit_with_scale = this.variantUnitWithScale.value;
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
if (match) {
this.variantUnit.value = match[1];
this.variantUnitScale.value = parseFloat(match[2]);
} else {
// "items"
this.variantUnit.value = variant_unit_with_scale;
this.variantUnitScale.value = "";
}
this.variantUnit.dispatchEvent(new Event("change"));
this.variantUnitScale.dispatchEvent(new Event("change"));
}
}

View File

@@ -34,7 +34,9 @@
border: none;
border-left: $border-radius solid $color-3;
border-radius: $border-radius;
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 2px 2px rgba(0, 0, 0, 0.07);
box-shadow:
0px 1px 0px rgba(0, 0, 0, 0.05),
0px 2px 2px rgba(0, 0, 0, 0.07);
margin: 2em 0;
padding: 0.5em 1em;
@@ -47,3 +49,22 @@
flex-shrink: 1;
}
}
.connected-app__vine {
margin: 1em 0;
.connected-app__vine-content {
display: flex;
justify-content: space-between;
align-items: end;
.vine-api-key {
width: 100%;
margin-right: 1em;
}
.vine-disable {
margin-left: auto;
}
}
}

View File

@@ -1,5 +1,7 @@
// Customisations for the new Bulk Edit Products page only
// Scoped to containing div, because Turbo messes with body classes
@import "../admin_v3/pages/unit_popout";
#products_v3_page {
#content > .row:first-child > .container:first-child {
// Allow table to extend to full width of available screen space
@@ -311,89 +313,7 @@
// Popout widget (todo: move to separate fiel)
.popout {
position: relative;
&__button {
// override button styles
&.popout__button {
background: $color-tbl-cell-bg;
color: $color-txt-text;
white-space: nowrap;
border-color: transparent;
font-weight: normal;
padding-left: $border-radius; // Super compact
padding-right: 1rem; // Retain space for arrow
height: auto;
min-width: 2em;
min-height: 1lh; // Line height of parent
&:hover,
&:active,
&:focus {
background: $color-tbl-cell-bg;
color: $color-txt-text;
position: relative;
}
&.changed {
border-color: $color-txt-changed-brd;
}
}
&:hover:not(:active):not(:focus):not(.changed) {
border-color: transparent;
}
&:hover,
&:active,
&:focus {
// for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead
&:before {
// for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles.
// extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes?
font-family: FontAwesome;
text-decoration: inherit;
display: inline-block;
speak: none;
content: "\f078";
position: absolute;
top: 0; // Required for empty buttons
right: $border-radius;
font-size: 0.67em;
}
}
}
&__container {
position: absolute;
top: -0.6em;
left: -0.2em;
z-index: 1; // Cover below row when hover
width: 9em;
padding: $padding-tbl-cell;
background: $color-tbl-cell-bg;
border-radius: $border-radius;
box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25);
.field {
margin-bottom: 0.75em;
&:last-child {
margin-bottom: 0;
}
}
input {
height: auto;
&[disabled] {
color: transparent; // hide value completely
}
}
}
@include unit_popout;
}
a.image-field {

View File

@@ -91,6 +91,7 @@
@import "../admin/dialog";
@import "../admin/disabled";
@import "components/dropdown"; // admin_v3
@import "pages/edit_variant"; // admin_v3
@import "pages/enterprise_index_panels"; // admin_v3
@import "../admin/enterprises";
@import "../admin/filters_and_controls";

View File

@@ -0,0 +1,87 @@
// Popout widget
@mixin unit_popout {
position: relative;
&__button {
// override button styles
&.popout__button {
background: $color-tbl-cell-bg;
color: $color-txt-text;
white-space: nowrap;
border-color: transparent;
font-weight: normal;
padding-left: $border-radius; // Super compact
padding-right: 1rem; // Retain space for arrow
height: auto;
min-width: 2em;
min-height: 1lh; // Line height of parent
&:hover,
&:active,
&:focus {
background: $color-tbl-cell-bg;
color: $color-txt-text;
position: relative;
}
&.changed {
border-color: $color-txt-changed-brd;
}
}
&:hover:not(:active):not(:focus):not(.changed) {
border-color: transparent;
}
&:hover,
&:active,
&:focus {
// for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead
&:before {
// for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles.
// extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes?
font-family: FontAwesome;
text-decoration: inherit;
display: inline-block;
speak: none;
content: "\f078";
position: absolute;
top: 0; // Required for empty buttons
right: $border-radius;
font-size: 0.67em;
}
}
}
&__container {
position: absolute;
top: -0.6em;
left: -0.2em;
z-index: 1; // Cover below row when hover
width: 9em;
padding: $padding-tbl-cell;
background: $color-tbl-cell-bg;
border-radius: $border-radius;
box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25);
.field {
margin-bottom: 0.75em;
&:last-child {
margin-bottom: 0;
}
}
input {
height: auto;
&[disabled] {
color: transparent; // hide value completely
}
}
}
}

View File

@@ -0,0 +1,55 @@
@import "unit_popout";
#edit_variant {
.popout {
@include unit_popout;
&__button {
// override popout button styles
&.popout__button {
// Reapplying button style from buttons.css
background-color: $color-btn-bg;
border: 1px solid $color-btn-bg;
color: $color-btn-text;
font-weight: bold;
&:before {
font-family: FontAwesome;
text-decoration: inherit;
display: inline-block;
speak: none;
content: "\f078";
position: absolute;
top: 0; // Required for empty buttons
right: $border-radius;
font-size: 0.67em;
}
// Reapplying button style from buttons.css
&:active,
&:focus {
outline: none;
border: 1px solid $color-btn-hover-border;
}
&:active:focus {
box-shadow: none;
}
&:hover {
background-color: $color-btn-hover-bg;
border: 1px solid $color-btn-hover-bg;
color: $color-btn-hover-text;
}
}
}
&__container {
width: max-content;
top: auto;
left: auto;
}
}
}

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