Bug 32030: ERM - link licenses to an agreement

Signed-off-by: Jonathan Field <jonathan.field@ptfs-europe.com>

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Jonathan Druart 2022-05-04 16:13:14 +02:00 committed by Tomas Cohen Arazi
parent ab5d7b5a63
commit ff2bf0d5a3
No known key found for this signature in database
GPG Key ID: 0A272EA1B2F3C15F
18 changed files with 324 additions and 82 deletions

View File

@ -23,6 +23,7 @@ use base qw(Koha::Object);
use Koha::ERM::Agreement::Periods;
use Koha::ERM::Agreement::UserRoles;
use Koha::ERM::Agreement::Licenses;
=head1 NAME
@ -85,6 +86,31 @@ sub user_roles {
return Koha::ERM::Agreement::UserRoles->_new_from_dbic($user_roles_rs);
}
=head3 agreement_licenses
Returns the agreement_licenses for this agreement
=cut
sub agreement_licenses {
my ( $self, $agreement_licenses ) = @_;
if ( $agreement_licenses ) {
my $schema = $self->_result->result_source->schema;
$schema->txn_do(
sub {
$self->agreement_licenses->delete;
for my $agreement_license (@$agreement_licenses) {
$self->_result->add_to_erm_agreement_licenses($agreement_license);
}
}
);
}
my $agreement_licenses_rs = $self->_result->erm_agreement_licenses;
return Koha::ERM::Agreement::Licenses->_new_from_dbic($agreement_licenses_rs);
}
=head2 Internal methods
=head3 _type

View File

@ -19,8 +19,8 @@ use Modern::Perl;
use Koha::Database;
use Koha::Agreements;
use Koha::Licenses;
use Koha::ERM::Agreements;
use Koha::ERM::Licenses;
use base qw(Koha::Object);

View File

@ -93,10 +93,12 @@ sub add {
my $periods = delete $body->{periods} // [];
my $user_roles = delete $body->{user_roles} // [];
my $agreement_licenses = delete $body->{agreement_licenses} // [];
my $agreement = Koha::ERM::Agreement->new_from_api($body)->store;
$agreement->periods($periods);
$agreement->user_roles($user_roles);
$agreement->agreement_licenses($agreement_licenses);
$c->res->headers->location($c->req->url->to_string . '/' . $agreement->agreement_id);
return $c->render(
@ -170,10 +172,12 @@ sub update {
my $periods = delete $body->{periods} // [];
my $user_roles = delete $body->{user_roles} // [];
my $agreement_licenses = delete $body->{agreement_licenses} // [];
$agreement->set_from_api($body)->store;
$agreement->periods($periods);
$agreement->user_roles($user_roles);
$agreement->agreement_licenses($agreement_licenses);
$c->res->headers->location($c->req->url->to_string . '/' . $agreement->agreement_id);
return $c->render(

View File

@ -49,6 +49,12 @@ properties:
description: role for users
items:
$ref: erm_agreement_user_role.yaml
agreement_licenses:
type: array
description: agreement licenses
items:
$ref: erm_agreement_license.yaml
additionalProperties: false
required:
- agreement_id

View File

@ -0,0 +1,30 @@
---
type: object
properties:
agreement_license_id:
type: integer
description: Internal agreement license identifier
agreement_id:
type: integer
description: Internal agreement identifier
license_id:
type: integer
description: Internal license identifier
status:
type: string
physical_location:
type:
- string
- "null"
notes:
type:
- string
- "null"
uri:
type:
- string
- "null"
additionalProperties: false
required:
- license_id
- status

View File

@ -31,4 +31,3 @@ properties:
additionalProperties: false
required:
- license_id
- agreement_id

View File

@ -187,6 +187,8 @@
- periods
- user_roles
- user_roles.patron
- agreement_licenses
- agreement_licenses.license
put:
x-mojo-to: ERM::Agreements#update
operationId: updateErmAgreements
@ -244,6 +246,7 @@
x-koha-embed:
- periods
- user_roles
- agreement_licenses
delete:
x-mojo-to: ERM::Agreements#delete
operationId: deleteErmAgreements

View File

@ -60,6 +60,9 @@
licenses_table_url += 'q='+ encodeURIComponent(JSON.stringify(license_name_filter));
[% END %]
const agreement_license_statuses = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_LICENSE_STATUS')) | $raw %];
const agreement_license_location = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_LICENSE_LOCATION')) | $raw %];
</script>
[% Asset.js("js/vue/dist/main.js") | $raw %]

View File

@ -0,0 +1,147 @@
<template>
<fieldset class="rows" id="agreement_licenses">
<legend>Licenses</legend>
<fieldset
class="rows"
v-for="(agreement_license, counter) in agreement_licenses"
v-bind:key="counter"
>
<legend>
Agreement license {{ counter + 1 }}
<a href="#" @click.prevent="deleteLicense(counter)"
><i class="fa fa-trash"></i> Remove this license</a
>
</legend>
<ol>
<li>
<label :for="`license_id_${counter}`">License: </label>
<select
:id="`license_id_${counter}`"
v-model="agreement_license.license_id"
required
>
<option value=""></option>
<option
v-for="license in licenses"
:key="license.license_id"
:value="license.license_id"
:selected="
license.license_id ==
agreement_license.license_id
? true
: false
"
>
{{ license.name }}
</option>
</select>
<span class="required">Required</span>
</li>
<li>
<label :for="`license_status_${counter}`">Status: </label>
<select v-model="agreement_license.status" required>
<option value=""></option>
<option
v-for="r in av_agreement_license_statuses"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value == agreement_license.status
? true
: false
"
>
{{ r.lib }}
</option>
</select>
<span class="required">Required</span>
</li>
<li>
<label :for="`license_location_${counter}`"
>Physical location:
</label>
<select v-model="agreement_license.physical_location">
<option value=""></option>
<option
v-for="r in av_agreement_license_location"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
r.authorised_value ==
agreement_license.physical_location
? true
: false
"
>
{{ r.lib }}
</option>
</select>
</li>
<li>
<label :for="`license_notes_${counter}`">Notes:</label>
<input
:id="`license_notes_${counter}`"
v-model="agreement_license.notes"
placeholder="Notes"
/>
</li>
<li>
<label :for="`license_uri_${counter}`">URI:</label>
<input
:id="`license_uri_${counter}`"
v-model="agreement_license.uri"
placeholder="URI"
/>
</li>
</ol>
</fieldset>
<a class="btn btn-default" @click="addLicense"
><font-awesome-icon icon="plus" /> Add new license</a
>
</fieldset>
</template>
<script>
export default {
name: 'AgreementLicenses',
data() {
return {
licenses: [],
}
},
props: {
av_agreement_license_statuses: Array,
av_agreement_license_location: Array,
agreement_licenses: Array,
},
beforeCreate() {
// FIXME it's not called on setup, but setup() does not have 'this'.
const apiUrl = "/api/v1/erm/licenses"
fetch(apiUrl)
.then((res) => res.json())
.then(
(result) => {
this.licenses = result
},
(error) => {
this.$emit("set-error", error)
}
)
},
methods: {
addLicense() {
this.agreement_licenses.push({
license_id: null,
status: null,
physical_location: null,
notes: '',
uri: '',
})
},
deleteLicense(counter) {
this.agreement_licenses.splice(counter, 1)
},
},
}
</script>

View File

@ -30,7 +30,7 @@
<select v-model="user_role.role" required>
<option value=""></option>
<option
v-for="r in av_user_roles"
v-for="r in av_agreement_user_roles"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
@ -61,7 +61,7 @@
export default {
name: 'AgreementUserRoles',
props: {
av_user_roles: Array,
av_agreement_user_roles: Array,
user_roles: Array,
},
beforeUpdate() {

View File

@ -5,9 +5,9 @@
<List
v-if="op == 'list'"
:vendors="vendors"
:av_statuses="statuses"
:av_closure_reasons="closure_reasons"
:av_renewal_priorities="renewal_priorities"
:av_agreement_statuses="agreement_statuses"
:av_agreement_closure_reasons="agreement_closure_reasons"
:av_agreement_renewal_priorities="agreement_renewal_priorities"
@set-current-agreement-id="setCurrentAgreementID"
@switch-view="switchView"
@set-error="setError"
@ -16,10 +16,12 @@
v-if="op == 'show'"
:agreement_id="agreement_id"
:vendors="vendors"
:av_statuses="statuses"
:av_closure_reasons="closure_reasons"
:av_renewal_priorities="renewal_priorities"
:av_user_roles="user_roles"
:av_agreement_statuses="agreement_statuses"
:av_agreement_closure_reasons="agreement_closure_reasons"
:av_agreement_renewal_priorities="agreement_renewal_priorities"
:av_agreement_user_roles="agreement_user_roles"
:av_agreement_license_statuses="agreement_license_statuses"
:av_agreement_license_location="agreement_license_location"
@switch-view="switchView"
@set-error="setError"
/>
@ -27,10 +29,12 @@
v-if="op == 'add-form'"
:agreement_id="agreement_id"
:vendors="vendors"
:av_statuses="statuses"
:av_closure_reasons="closure_reasons"
:av_renewal_priorities="renewal_priorities"
:av_user_roles="user_roles"
:av_agreement_statuses="agreement_statuses"
:av_agreement_closure_reasons="agreement_closure_reasons"
:av_agreement_renewal_priorities="agreement_renewal_priorities"
:av_agreement_user_roles="agreement_user_roles"
:av_agreement_license_statuses="agreement_license_statuses"
:av_agreement_license_location="agreement_license_location"
@agreement-created="agreementCreated"
@agreement-updated="agreementUpdated"
@switch-view="switchView"
@ -62,10 +66,12 @@ export default {
message: null,
error: null,
vendors: [],
statuses: agreement_statuses,
closure_reasons: agreement_closure_reasons,
renewal_priorities: agreement_renewal_priorities,
user_roles: agreement_user_roles,
agreement_statuses,
agreement_closure_reasons,
agreement_renewal_priorities,
agreement_user_roles,
agreement_license_statuses,
agreement_license_location,
}
},
beforeCreate() {

View File

@ -60,7 +60,7 @@
>
<option value=""></option>
<option
v-for="status in av_statuses"
v-for="status in av_agreement_statuses"
:key="status.authorised_values"
:value="status.authorised_value"
:selected="
@ -87,7 +87,7 @@
>
<option value=""></option>
<option
v-for="r in av_closure_reasons"
v-for="r in av_agreement_closure_reasons"
:key="r.authorised_values"
:value="r.authorised_value"
:selected="
@ -133,7 +133,7 @@
<select v-model="agreement.renewal_priority">
<option value=""></option>
<option
v-for="p in av_renewal_priorities"
v-for="p in av_agreement_renewal_priorities"
:key="p.authorised_values"
:value="p.authorised_value"
:selected="
@ -161,13 +161,23 @@
<AgreementPeriods :periods="agreement.periods" />
<AgreementUserRoles
:user_roles="agreement.user_roles"
:av_user_roles="av_user_roles"
:av_agreement_user_roles="av_agreement_user_roles"
/>
<AgreementLicenses
:agreement_licenses="agreement.agreement_licenses"
:av_agreement_license_statuses="av_agreement_license_statuses"
:av_agreement_license_location="
av_agreement_license_location
"
/>
</ol>
</fieldset>
<fieldset class="action">
<input type="submit" value="Submit" />
<a role="button" class="cancel" @click="$emit('switch-view', 'list')"
<a
role="button"
class="cancel"
@click="$emit('switch-view', 'list')"
>Cancel</a
>
</fieldset>
@ -178,6 +188,7 @@
<script>
import AgreementPeriods from './AgreementPeriods.vue'
import AgreementUserRoles from './AgreementUserRoles.vue'
import AgreementLicenses from './AgreementLicenses.vue'
export default {
data() {
@ -194,6 +205,7 @@ export default {
license_info: '',
periods: [],
user_roles: [],
agreement_licenses: [],
}
}
},
@ -203,7 +215,7 @@ export default {
fetch(apiUrl, {
headers: {
'x-koha-embed': 'periods,user_roles,user_roles.patron'
'x-koha-embed': 'periods,user_roles,user_roles.patron,agreement_licenses,agreement_licenses.license'
}
})
.then(res => res.json())
@ -239,6 +251,8 @@ export default {
agreement.user_roles = agreement.user_roles.map(({ patron, patron_str, ...keepAttrs }) => keepAttrs)
agreement.agreement_licenses = agreement.agreement_licenses.map(({ license, agreement_id, agreement_license_id, ...keepAttrs }) => keepAttrs)
const options = {
method: method,
body: JSON.stringify(agreement),
@ -272,14 +286,17 @@ export default {
props: {
agreement_id: Number,
vendors: Array,
av_statuses: Array,
av_closure_reasons: Array,
av_renewal_priorities: Array,
av_user_roles: Array,
av_agreement_statuses: Array,
av_agreement_closure_reasons: Array,
av_agreement_renewal_priorities: Array,
av_agreement_user_roles: Array,
av_agreement_license_statuses: Array,
av_agreement_license_location: Array,
},
components: {
AgreementPeriods,
AgreementUserRoles
AgreementUserRoles,
AgreementLicenses,
},
name: "AgreementsFormAdd",
}

View File

@ -33,7 +33,7 @@ export default {
let show_agreement = this.show_agreement
let edit_agreement = this.edit_agreement
let delete_agreement = this.delete_agreement
window['agreements_av_vendors'] = this.vendors.map(e => {
window['vendors'] = this.vendors.map(e => {
e['_id'] = e['id']
e['_str'] = e['name']
return e
@ -42,34 +42,34 @@ export default {
map[e.id] = e
return map
}, {})
window['agreements_av_statuses'] = this.av_statuses.map(e => {
window['av_agreement_statuses'] = this.av_agreement_statuses.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
})
let statuses_map = this.av_statuses.reduce((map, e) => {
let av_agreement_statuses_map = this.av_agreement_statuses.reduce((map, e) => {
map[e.authorised_value] = e
return map
}, {})
window['agreements_av_closure_reasons'] = this.av_closure_reasons.map(e => {
window['av_agreement_closure_reasons'] = this.av_agreement_closure_reasons.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
})
let closure_reasons_map = this.av_closure_reasons.reduce((map, e) => {
let av_agreement_closure_reasons_map = this.av_agreement_closure_reasons.reduce((map, e) => {
map[e.authorised_value] = e
return map
}, {})
window['agreements_av_renewal_priorities'] = this.av_renewal_priorities.map(e => {
window['av_agreement_renewal_priorities'] = this.av_agreement_renewal_priorities.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
})
let renewal_priorities_map = this.av_renewal_priorities.reduce((map, e) => {
let av_agreement_renewal_priorities_map = this.av_agreement_renewal_priorities.reduce((map, e) => {
map[e.authorised_value] = e
return map
}, {})
window['agreements_av_is_perpetual'] = [{ _id: 0, _str: _('No') }, { _id: 1, _str: _("Yes") }]
window['av_agreement_is_perpetual'] = [{ _id: 0, _str: _('No') }, { _id: 1, _str: _("Yes") }]
$('#agreement_list').kohaTable({
"ajax": {
@ -114,7 +114,7 @@ export default {
"searchable": true,
"orderable": true,
"render": function (data, type, row, meta) {
return escape_str(statuses_map[row.status].lib)
return escape_str(av_agreement_statuses_map[row.status].lib)
}
},
{
@ -123,7 +123,7 @@ export default {
"searchable": true,
"orderable": true,
"render": function (data, type, row, meta) {
return row.closure_reason != undefined && row.closure_reason != "" ? escape_str(closure_reasons_map[row.closure_reason].lib) : ""
return row.closure_reason != undefined && row.closure_reason != "" ? escape_str(av_agreement_closure_reasons_map[row.closure_reason].lib) : ""
}
},
{
@ -141,7 +141,7 @@ export default {
"searchable": true,
"orderable": true,
"render": function (data, type, row, meta) {
return row.renewal_priority != undefined && row.renewal_priority != "" ? escape_str(renewal_priorities_map[row.renewal_priority].lib) : ""
return row.renewal_priority != undefined && row.renewal_priority != "" ? escape_str(av_agreement_renewal_priorities_map[row.renewal_priority].lib) : ""
}
},
{
@ -174,8 +174,9 @@ export default {
render(n, e)
})
$.each($(this).find("tr td:eq(0)"), function (index, e) {
$.each($(this).find("tbody tr td:first-child"), function (index, e) {
let row = api.row(index).data()
if (!row) return // Happen if the table is empty
let n = createVNode("a", {
role: "button",
onClick: () => {
@ -189,11 +190,11 @@ export default {
},
preDrawCallback: function (settings) {
var table_id = settings.nTable.id
$("#" + table_id).find("thead th").eq(1).attr('data-filter', 'agreements_av_vendors')
$("#" + table_id).find("thead th").eq(3).attr('data-filter', 'agreements_av_statuses')
$("#" + table_id).find("thead th").eq(4).attr('data-filter', 'agreements_av_closure_reasons')
$("#" + table_id).find("thead th").eq(5).attr('data-filter', 'agreements_av_is_perpetual')
$("#" + table_id).find("thead th").eq(6).attr('data-filter', 'agreements_av_renewal_priorities')
$("#" + table_id).find("thead th").eq(1).attr('data-filter', 'vendors')
$("#" + table_id).find("thead th").eq(3).attr('data-filter', 'av_agreement_statuses')
$("#" + table_id).find("thead th").eq(4).attr('data-filter', 'av_agreement_closure_reasons')
$("#" + table_id).find("thead th").eq(5).attr('data-filter', 'av_agreement_is_perpetual')
$("#" + table_id).find("thead th").eq(6).attr('data-filter', 'av_agreement_renewal_priorities')
}
}, table_settings, 1)
@ -225,9 +226,9 @@ export default {
},
props: {
vendors: Array,
av_statuses: Array,
av_closure_reasons: Array,
av_renewal_priorities: Array,
av_agreement_statuses: Array,
av_agreement_closure_reasons: Array,
av_agreement_renewal_priorities: Array,
},
name: "AgreementsList",
}

View File

@ -102,7 +102,7 @@
<td>{{ patron_to_html(role.patron) }}</td>
<td>
{{
av_user_roles.find(
av_agreement_user_roles.find(
(r) =>
r.authorised_value == role.role
).lib
@ -188,10 +188,10 @@ export default {
props: {
agreement_id: Number,
vendors: Array,
av_statuses: Array,
av_closure_reasons: Array,
av_renewal_priorities: Array,
av_user_roles: Array,
av_agreement_statuses: Array,
av_agreement_closure_reasons: Array,
av_agreement_renewal_priorities: Array,
av_agreement_user_roles: Array,
},
components: {
AgreementPeriods,

View File

@ -4,8 +4,8 @@
<div class="dialog alert" v-if="error">{{ error }}</div>
<List
v-if="op == 'list'"
:av_types="types"
:av_statuses="statuses"
:av_license_types="license_types"
:av_license_statuses="license_statuses"
@set-current-license-id="setCurrentLicenseID"
@switch-view="switchView"
@set-error="setError"
@ -13,16 +13,16 @@
<Show
v-if="op == 'show'"
:license_id="license_id"
:av_types="types"
:av_statuses="statuses"
:av_license_types="license_types"
:av_license_statuses="license_statuses"
@switch-view="switchView"
@set-error="setError"
/>
<AddForm
v-if="op == 'add-form'"
:license_id="license_id"
:av_types="types"
:av_statuses="statuses"
:av_license_types="license_types"
:av_license_statuses="license_statuses"
@license-created="licenseCreated"
@license-updated="licenseUpdated"
@switch-view="switchView"
@ -53,8 +53,8 @@ export default {
op: "list",
message: null,
error: null,
types: license_types,
statuses: license_statuses,
license_types,
license_statuses,
}
},
methods: {

View File

@ -38,7 +38,7 @@
>
<option value=""></option>
<option
v-for="type in av_types"
v-for="type in av_license_types"
:key="type.authorised_values"
:value="type.authorised_value"
:selected="
@ -61,7 +61,7 @@
>
<option value=""></option>
<option
v-for="status in av_statuses"
v-for="status in av_license_statuses"
:key="status.authorised_values"
:value="status.authorised_value"
:selected="
@ -194,8 +194,8 @@ export default {
emits: ['license-created', 'license-updated', 'set-error', 'switch-view'],
props: {
license_id: Number,
av_types: Array,
av_statuses: Array,
av_license_types: Array,
av_license_statuses: Array,
},
components: {
flatPickr

View File

@ -32,21 +32,21 @@ export default {
let show_license = this.show_license
let edit_license = this.edit_license
let delete_license = this.delete_license
window['licenses_av_types'] = this.av_types.map(e => {
window['av_license_types'] = this.av_license_types.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
})
let types_map = this.av_types.reduce((map, e) => {
let av_license_types_map = this.av_license_types.reduce((map, e) => {
map[e.authorised_value] = e
return map
}, {})
window['licenses_av_statuses'] = this.av_statuses.map(e => {
window['av_license_statuses'] = this.av_license_statuses.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
})
let statuses_map = this.av_statuses.reduce((map, e) => {
let av_license_statuses_map = this.av_license_statuses.reduce((map, e) => {
map[e.authorised_value] = e
return map
}, {})
@ -85,7 +85,7 @@ export default {
"searchable": true,
"orderable": true,
"render": function (data, type, row, meta) {
return escape_str(types_map[row.type].lib)
return escape_str(av_license_types_map[row.type].lib)
}
},
{
@ -94,7 +94,7 @@ export default {
"searchable": true,
"orderable": true,
"render": function (data, type, row, meta) {
return escape_str(statuses_map[row.status].lib)
return escape_str(av_license_statuses_map[row.status].lib)
}
},
{
@ -161,8 +161,8 @@ export default {
},
preDrawCallback: function (settings) {
var table_id = settings.nTable.id
$("#" + table_id).find("thead th").eq(2).attr('data-filter', 'licenses_av_types')
$("#" + table_id).find("thead th").eq(3).attr('data-filter', 'licenses_av_statuses')
$("#" + table_id).find("thead th").eq(2).attr('data-filter', 'av_license_types')
$("#" + table_id).find("thead th").eq(3).attr('data-filter', 'av_license_statuses')
}
}, table_settings, 1)
@ -193,8 +193,8 @@ export default {
},
},
props: {
av_types: Array,
av_statuses: Array,
av_license_types: Array,
av_license_statuses: Array,
},
name: "LicensesList",
}

View File

@ -18,13 +18,13 @@
<li>
<label>Type: </label>
<span>{{
get_lib_from_av(av_types, license.type)
get_lib_from_av(av_license_types, license.type)
}}</span>
</li>
<li>
<label>Status: </label>
<span>{{
get_lib_from_av(av_statuses, license.status)
get_lib_from_av(av_license_statuses, license.status)
}}</span>
</li>
@ -104,8 +104,8 @@ export default {
emits: ['set-error', 'switch-view'],
props: {
license_id: Number,
av_types: Array,
av_statuses: Array,
av_license_types: Array,
av_license_statuses: Array,
},
components: {
},