Merge branch 'develop' of github.com:frappe/erpnext into call-summary-dialog

This commit is contained in:
Suraj Shetty
2019-06-10 09:25:39 +05:30
249 changed files with 8958 additions and 35360 deletions

View File

@@ -50,14 +50,12 @@
"public/js/education/student_button.html",
"public/js/education/assessment_result_tool.html",
"public/js/hub/hub_factory.js",
"public/js/call_popup/call_popup.js"
"public/js/call_popup/call_popup.js",
"public/js/utils/dimension_tree_filter.js"
],
"js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.html",
"stock/dashboard/item_dashboard.js"
],
"js/lms.min.js": [
"public/js/education/lms/lms.js"
]
}

View File

@@ -14,8 +14,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
this._super();
},
onload: function() {
this.setup_queries();
onload: function(doc, cdt, cdn) {
this.setup_queries(doc, cdt, cdn);
this._super();
this.frm.set_query('shipping_rule', function() {
@@ -50,7 +50,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
/* eslint-enable */
},
setup_queries: function() {
setup_queries: function(doc, cdt, cdn) {
var me = this;
if(this.frm.fields_dict.buying_price_list) {
@@ -90,6 +90,15 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
}
}
});
this.frm.set_query("manufacturer", "items", function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
return {
query: "erpnext.controllers.queries.item_manufacturer_query",
filters:{ 'item_code': row.item_code }
}
});
},
refresh: function(doc) {
@@ -338,6 +347,25 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
}
})
}
},
manufacturer: function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
if(row.manufacturer) {
frappe.call({
method: "erpnext.stock.doctype.item_manufacturer.item_manufacturer.get_item_manufacturer_part_no",
args: {
'item_code': row.item_code,
'manufacturer': row.manufacturer
},
callback: function(r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, 'manufacturer_part_no', r.message);
}
}
});
}
}
});

View File

@@ -515,7 +515,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
net_total += item.net_amount;
// discount amount rounding loss adjustment if no taxes
if ((!(me.frm.doc.taxes || []).length || (me.frm.doc.apply_discount_on == "Net Total"))
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
&& i == (me.frm.doc.items || []).length - 1) {
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
- me.frm.doc.discount_amount, precision("net_total"));

View File

@@ -460,6 +460,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
weight_per_unit: item.weight_per_unit,
weight_uom: item.weight_uom,
uom : item.uom,
manufacturer: item.manufacturer,
stock_uom: item.stock_uom,
pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
cost_center: item.cost_center,

View File

@@ -1,15 +0,0 @@
frappe.ready(() => {
frappe.provide('lms');
lms.call = (method, args) => {
const method_path = 'erpnext.www.lms.' + method;
return new Promise((resolve, reject) => {
return frappe.call({
method: method_path,
args,
})
.then(r => resolve(r.message))
.fail(reject);
});
};
});

View File

@@ -1,44 +0,0 @@
<template>
<div>
<ContentTitle :title="contentData.title" :author="contentData.author" :publishDate="contentData.publish_date">
<slot></slot>
</ContentTitle>
<section class="article-content-section">
<div>
<div class="content" v-html="contentData.content"></div>
<div class="text-right">
</div>
<div class="mt-3 text-right">
<a class="text-muted" href="/report"><i class="octicon octicon-issue-opened" title="Report"></i> Report a
Mistake</a>
</div>
</div>
</section>
</div>
</template>
<script>
import ContentTitle from './ContentTitle.vue'
export default {
props: ['content', 'type'],
name: 'Article',
data() {
return {
contentData: ''
}
},
mounted() {
this.getContent().then(data => this.contentData = data);
},
methods: {
getContent() {
return lms.call('get_content', {
content_type: this.type,
content: this.content
})
}
},
components: {
ContentTitle
}
};
</script>

View File

@@ -1,56 +0,0 @@
<template>
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li v-for="(route, index) in routeData" class="breadcrumb-item active" aria-current="page">
<router-link v-if="index != routeData.length - 1" :to="route.route">
{{ route.label }}
</router-link>
<span v-else>{{ route.label }}</span>
</li>
</ol>
</nav>
</div>
</template>
<script type="text/javascript">
export default {
name: "Breadcrumb",
data() {
return {
routeName: this.$route.name,
routeParams: this.$route.params,
routeData: [{
label: "All Programs",
route: "/List/Program"
}]
}
},
mounted() {
this.buildBreadcrumb()
},
methods: {
buildBreadcrumb() {
if(this.routeName == 'program') {
return
}
if(this.routeName == 'course') {
let routeObject = {
label: this.routeParams.program_name,
route: `/Program/${this.routeParams.program_name}`
}
this.routeData.push(routeObject)
}
if(this.routeName == 'content') {
this.routeData.push({
label: this.routeParams.program_name,
route: `/Program/${this.routeParams.program_name}`
})
this.routeData.push({
label: this.routeParams.course_name,
route: `/Program/${this.routeParams.program_name}/${this.routeParams.course_name}`
})
}
}
}
};
</script>

View File

@@ -1,25 +0,0 @@
<template>
<button :class="classList" v-on="$listeners" v-bind="$attrs" @click="goToRoute">
<slot></slot>
</button>
</template>
<script>
export default {
name: 'AButton',
props: ['type', 'size', 'route'],
computed: {
classList() {
return [
'btn',
'btn-' + this.type,
'btn-' + this.size
]
}
},
methods: {
goToRoute() {
this.$router.push(this.route);
}
}
}
</script>

View File

@@ -1,28 +0,0 @@
<template>
<div class="featured-products-section py-3">
<h5 class='featured-heading' v-html="title"></h5>
<div class="featured-products row">
<!-- <p class='lead text-center' v-html="description"></p> -->
<slot name="card-list-slot"></slot>
</div>
<div class='mt-4 text-center'>
<slot name="list-bottom"></slot>
</div>
</div>
</template>
<script>
export default {
props:['title', 'description'],
name: "CardList",
};
</script>
<style scoped>
.featured-heading {
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 12px;
font-weight: 500;
}
</style>

View File

@@ -1,40 +0,0 @@
<template>
<div class="nav-buttons">
<button class='btn btn-outline-secondary' @click="$router.go(-1)">Back</button>
<button v-if="nextContent" class='btn btn-primary' @click="goNext()">Next</button>
<button v-else class='btn btn-primary' @click="finish()">Finish Topic</button>
</div>
</template>
<script>
export default {
props: ['nextContent', 'nextContentType'],
name: 'ContentNavigation',
methods: {
addActivity() {
if(this.$route.params.type != "Quiz"){
console.log("Adding Activity")
lms.call("add_activity",
{
course: this.$route.params.course_name,
content_type: this.$route.params.type,
content: this.$route.params.content,
}
)
}
},
goNext() {
this.addActivity()
this.$router.push({ name: 'content', params: { course: this.$route.params.course_name, type:this.nextContentType, content:this.nextContent }})
},
finish() {
this.addActivity()
this.$router.push({ name: 'course', params: { program_name: this.$route.params.program_name, course_name: this.$route.params.course_name}})
lms.trigger('course-completed', course_name);
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,29 +0,0 @@
<template>
<section class='article-top-section video-section-bg'>
<div>
<div class="row">
<div class="col-md-8">
<h2>{{ title }}</h2>
<span v-if="typeof author !== 'undefined' || author !== null" class="text-muted">
<span v-if="publishDate">Published on {{ publishDate }}</span>
<span v-if="author"> {{ author }}</span>
</span>
</div>
<div class="col-md-4 text-right">
<slot></slot>
</div>
</div>
<hr>
</div>
</section>
</template>
<script>
export default {
props: ['title', 'publishDate', 'author'],
name: 'ContentTitle',
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,87 +0,0 @@
<template>
<div class="py-3 col-md-4 col-sm-12">
<div class="card h-100">
<div class="card-hero-img" v-if="course.hero_image" v-bind:style="{ 'background-image': 'url(' + image + ')' }"></div>
<div v-else class="card-image-wrapper">
<div class="image-body">{{ course.course_name }}</div>
</div>
<div class='card-body'>
<h5 class="card-title">{{ course.course_name }}</h5>
<span class="course-list text-muted" id="getting-started">
{{ course.course_intro.substring(0,120) }}
</span>
</div>
<div class='p-3' style="display: flex; justify-content: space-between;">
<div>
<span v-if="complete"><i class="mr-2 text-success fa fa-check-circle" aria-hidden="true"></i>Course Complete</span>
</div>
<div class='text-right'>
<a-button
:type="'primary'"
size="sm"
:route="courseRoute"
>
{{ buttonName }}
</a-button>
</div>
</div>
</div>
</div>
</template>
<script>
import AButton from './Button.vue';
export default {
props: ['course', 'program_name'],
name: "CourseCard",
components: {
AButton
},
data() {
return {
courseDetails: {},
}
},
mounted() {
if(lms.store.checkLogin()) this.getCourseDetails().then(data => this.courseDetails = data)
},
computed: {
courseRoute() {
return `${this.program_name}/${this.course.name}`
},
complete() {
if(lms.store.checkProgramEnrollment(this.program_name)){
if (this.courseDetails.flag === "Completed" ) {
return true
}
else {
return false
}
}
else {
return false
}
},
isLogin() {
return lms.store.checkLogin()
},
buttonName() {
if(lms.store.checkProgramEnrollment(this.program_name)){
return "Start Course"
}
else {
return "Explore"
}
}
},
methods: {
getCourseDetails() {
return lms.call('get_student_course_details', {
course_name: this.course.name,
program_name: this.program_name
})
},
}
};
</script>

View File

@@ -1,85 +0,0 @@
<template>
<nav class="navbar navbar-light bg-white navbar-expand-lg sticky-top shadow-sm">
<div class="container">
<a class="navbar-brand" href="/lms">
<span>{{ portal.title }}</span>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="lms#/List/Program">
All Programs
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/lms#/Profile">
Profile
</a>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<!-- post login tools -->
<li v-if="isLogin" class="nav-item dropdown logged-in" id="website-post-login" data-label="website-post-login">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="user-image-wrapper">
<span class="avatar avatar-small" :title="fullName">
<span class="avatar-frame" :style="avatarStyle" :title="fullName"></span>
</span>
</span>
<span class="full-name">{{ fullName }}</span>
<b class="caret"></b>
</a>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<a class="dropdown-item" href="/me" rel="nofollow"> My Account </a>
<a class="dropdown-item" href="/?cmd=web_logout" rel="nofollow"> Logout </a>
</ul>
</li>
<li v-else class="nav-item">
<a class="nav-link btn-login-area" href="/login">Login</a>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
name: "Home",
data() {
return{
portal: {},
avatar: frappe.user_image,
fullName: frappe.full_name,
isLogin: frappe.is_user_logged_in()
}
},
mounted() {
this.getPortalDetails().then(data => this.portal = data);
},
methods: {
getPortalDetails() {
return lms.call("get_portal_details")
}
},
computed: {
avatarStyle() {
return `background-image: url("${this.avatar}")`
},
// isLogin() {
// return frappe.is_user_logged_in()
// },
}
};
</script>
<style scoped>
a {
text-decoration: none;
}
</style>

View File

@@ -1,83 +0,0 @@
<template>
<div class="py-5">
<div class="row">
<div class="col-sm-12">
<div>
<h3>{{ fullName }}</h3>
<ul>
<li class="row">
<div class="col-md-3 col-sm-4 pr-0 text-muted">Email:</div>
<div class="col-md-9 col-sm-8">{{ email }}</div>
</li>
<li v-if="joiningDate" class="row">
<div class="col-md-3 col-sm-4 pr-0 text-muted">Date of Joining:</div>
<div class="col-md-9 col-sm-8">{{ joiningDate }}</div>
</li>
<li class="row">
<div class="col-md-3 col-sm-4 pr-0 text-muted">Programs Enrolled:</div>
<div class="col-md-9 col-sm-8">
<ul v-if="enrolledPrograms">
<li v-for="program in enrolledPrograms" :key="program">{{ program }}</li>
</ul>
<span v-else>None</span>
</div>
</li>
</ul>
</div>
<a href="/update-profile" class="edit-button text-muted">Edit Profile</a>
</div>
</div>
<div ></div>
</div>
</template>
<script>
export default {
props: ['enrolledPrograms'],
name: "ProfileInfo",
data() {
return {
avatar: frappe.user_image,
fullName: frappe.full_name,
abbr: frappe.get_abbr(frappe.get_cookie("full_name")),
email: frappe.session.user,
joiningDate: ''
}
},
mounted(){
this.getJoiningDate().then(data => {
if(data) {
this.joiningDate = lms.moment(String(data)).format('D MMMM YYYY')
}
})
},
computed: {
avatarStyle() {
return `background-image: url("${this.avatar}")`
},
},
methods: {
getJoiningDate() {
return lms.call("get_joining_date")
}
}
};
</script>
<style scoped>
.edit-button {
position: absolute;
top: 0;
right: 0;
}
.standard-image {
font-size: 72px;
border-radius: 6px;
}
ul {
list-style-type: none;
padding: 0;
margin: 0
}
</style>

View File

@@ -1,82 +0,0 @@
<template>
<div class='py-3 col-md-4 col-sm-12'>
<div class="card h-100">
<router-link :to="'/Program/' + program.name">
<div class="card-hero-img" v-if="program.hero_image" v-bind:style="{ 'background-image': 'url(' + image + ')' }"></div>
<div v-else class="card-image-wrapper text-center">
<div class="image-body">{{ program.program_name }}</div>
</div>
<div class='card-body'>
<h5 class='card-title'>{{ program.program_name }}</h5>
<div class="text-muted">{{ program.description.substring(0,120) }}...</div>
</div>
</router-link>
<div class='text-right p-3'>
<button v-if="program.intro_video" class='btn btn-light btn-sm' data-toggle="modal" data-target="#videoModal">Watch Intro</button>
<a-button v-if="enrolled" type="dark" size="sm" :route="programPageRoute">
{{ buttonName }}
</a-button>
<button v-else-if="isLogin" class='btn btn-dark btn-sm' @click="enroll()">{{ enrollButton }}</button>
<a v-else class='btn btn-secondary btn-sm' href="/login#signup">Sign Up</a>
</div>
<VideoModal v-if="program.intro_video" :title="program.program_name" :video="program.intro_video"/>
</div>
</div>
</template>
<script>
import AButton from './Button.vue';
import VideoModal from './VideoModal.vue';
export default {
props: ['program', 'enrolled'],
name: "ProgramCard",
data() {
return {
isLogin: frappe.is_user_logged_in(),
enrollButton: 'Enroll Now',
programRoute: { name: 'program', params: { program_name: this.program.name }},
image: "'" + this.program.hero_image + "'"
};
},
methods: {
enroll() {
this.enrollButton = 'Enrolling...'
lms.call('enroll_in_program', {
program_name: this.program.name,
}).then(data => {
lms.store.updateEnrolledPrograms()
this.$router.push(this.programRoute)
})
}
},
computed: {
buttonName() {
if(this.enrolled){
return "Start Program"
}
else {
return "Enroll"
}
},
programPageRoute() {
return this.programRoute
},
isEnrolled() {
return lms.store.enrolledPrograms.includes(this.program.name)
}
},
components: {
AButton,
VideoModal
}
};
</script>
<style lang="css" scoped>
a {
text-decoration: none;
color: black;
}
a.btn-secondary {
color: white !important;
}
</style>

View File

@@ -1,89 +0,0 @@
<template>
<div class='py-3 col-md-4 col-sm-12'>
<div class="card h-100">
<div class='card-body'>
<router-link :to="'/Program/' + programData.name">
<h5 class='card-title'>{{ programData.program }}</h5>
</router-link>
<span class="course-list text-muted" id="getting-started">
Courses
<ul class="mb-0 mt-1 list-unstyled" style="padding-left: 1.5em;">
<li v-for="item in programData.progress" :key="item.name">
<span v-if="item.is_complete"><i class="text-success fa fa-check-circle" aria-hidden="true"></i></span>
<span v-else><i class="text-secondary fa fa-circle-o" aria-hidden="true"></i></span>
{{ item.course_name }}
</li>
</ul>
</span>
</div>
<div class='p-3' style="display: flex; justify-content: space-between;">
<div></div>
<div class='text-right'>
<a-button
:type="buttonType"
size="sm btn-block"
:route="programRoute"
>
{{ buttonName }}
</a-button>
</div>
</div>
</div>
</div>
</template>
<script>
import AButton from './Button.vue';
export default {
props: ['program'],
name: "ProgressCard",
data() {
return {
programData: {}
};
},
mounted() {
this.getProgramProgress().then(data => this.programData = data)
},
methods: {
getProgramProgress() {
return lms.call('get_program_progress', {
program_name: this.program
})
},
},
computed: {
programRoute() {
return {name: 'program', params: {program_name: this.program}}
},
buttonType() {
if (this.programData.percentage == 100 ){
return "success"
}
else if (this.programData.percentage == "0" ) {
return "secondary"
}
else {
return "info"
}
},
buttonName() {
if (this.programData.percentage == 100 ){
return "Program Complete"
}
else {
return `${this.programData.percentage}% Completed`
}
}
},
components: {
AButton
},
};
</script>
<style scoped>
a {
text-decoration: none;
color: black;
}
</style>

View File

@@ -1,119 +0,0 @@
<template>
<section class="quiz-section">
<div>
<div class="row">
<div class="col-md-8">
<h2>{{ content }}</h2>
</div>
</div>
<div class="content">
<hr>
<div id="quiz" :name="content">
<div id="quiz-body">
<component v-for="question in quizData" :key="question.name" v-bind:is="question.type" :question="question" @updateResponse="updateResponse" :isDisabled="isDisabled"></component>
</div>
<div class="mt-3">
<div>
<div v-if="isDisabled || submitted" id="post-quiz-actions" class="row">
<div class="col-md-8 text-left">
<span v-html="message"></span>
</div>
<div class="col-md-4 text-right">
<slot></slot>
</div>
</div>
<div v-else id="quiz-actions" class="text-right">
<button class='btn btn-outline-secondary' type="reset" :disabled="isDisabled">Reset</button>
<button class='btn btn-primary' @click="submitQuiz" type="button" :disabled="isDisabled">Submit</button>
</div>
</div>
</div>
</div>
</div>
<div class="mt-3 text-right">
<a class="text-muted" href="/report"><i class="octicon octicon-issue-opened" title="Report"></i> Report a
Mistake</a>
</div>
</div>
</section>
</template>
<script>
import QuizSingleChoice from "./Quiz/QuizSingleChoice.vue"
import QuizMultipleChoice from "./Quiz/QuizMultipleChoice.vue"
export default {
props: ['content', 'type'],
name: 'Quiz',
data() {
return {
quizData: '',
quizResponse: {},
score: '',
submitted: false,
isDisabled: false,
quizStatus: {},
}
},
mounted() {
this.getQuizWithoutAnswers().then(data => {
this.quizData = data.quizData
this.quizStatus = data.status
this.isDisabled = data.status.is_complete
});
},
components: {
'SingleChoice': QuizSingleChoice,
'MultipleChoice': QuizMultipleChoice
},
methods: {
getQuizWithoutAnswers() {
return lms.call("get_quiz_without_answers",
{
quiz_name: this.content,
course_name: this.$route.params.course_name
}
)
},
updateResponse(res) {
this.quizResponse[res.question] = res.option
},
submitQuiz() {
lms.call("evaluate_quiz",
{
quiz_response: this.quizResponse,
quiz_name: this.content,
course: this.$route.params.course_name
}
).then(data => {
this.score = data
this.submitted = true
this.quizResponse = null
});
}
},
computed: {
currentComponent: function() {
if(this.quizData.type === "MultipleChoice") {
return 'QuizMultipleChoice'
}
else {
return 'QuizSingleChoice'
}
},
message: function() {
if(this.submitted) {
return '<h3>Your Score: <span id="result">'+ this.score +'</span></h3>'
}
let message = '<h4>You have exhausted all attempts for this quiz.</h4>'
if(this.quizStatus.result == 'Pass') {
message = "<h4>You have successfully completed this quiz.</h4>Score: " + this.quizStatus.score
}
return message
}
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,34 +0,0 @@
<template>
<div class="question mt-4">
<h5>{{ question.question }}</h5>
<div class="options ml-2">
<div v-for="option in question.options" :key="option.name" class="form-check pb-1">
<input v-model="checked" class="form-check-input" type="checkbox" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)" :disabled="isDisabled">
<label class="form-check-label" :for="option.name">
{{ option.option }}
</label>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['question', 'isDisabled'],
name: 'QuizSingleChoice',
data() {
return {
checked: []
}
},
methods: {
emitResponse(q, o) {
console.log(this.checked)
this.$emit('updateResponse', {'question':q , 'option': this.checked, 'type': this.question.type})
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,28 +0,0 @@
<template>
<div class="question mt-4">
<h5>{{ question.question }}</h5>
<div class="options ml-2">
<div v-for="option in question.options" :key="option.name" class="form-check pb-1">
<input class="form-check-input" type="radio" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)" :disabled="isDisabled">
<label class="form-check-label" :for="option.name">
{{ option.option }}
</label>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['question', 'isDisabled'],
name: 'QuizSingleChoice',
methods: {
emitResponse(q, o) {
this.$emit('updateResponse', {'question':q , 'option': o, 'type': this.question.type})
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,60 +0,0 @@
<template>
<div v-if="quizData" class='py-3 col-md-4 col-sm-12'>
<div class="card h-100">
<div class='card-body'>
<h5 class='card-title'>{{ quizData.program }}</h5>
<div v-for="attempt in quizData.quiz_attempt" :key="attempt.content" class="course-list" id="getting-started">
<div>
{{ attempt.content }}
<ul v-if="attempt.is_complete">
<li><span class="text-muted">Score: </span>{{ attempt.score }}</li>
<li><span class="text-muted">Status: </span>{{attempt.result }}</li>
</ul>
<span v-else>- Unattempted</span>
</div>
</div>
</div>
<div class='p-3' style="display: flex; justify-content: space-between;">
<div></div>
<div class='text-right'>
<a-button
:type="'primary'"
size="sm btn-block"
:route="programRoute"
>
Go To Program
</a-button>
</div>
</div>
</div>
</div>
</template>
<script>
import AButton from './Button.vue';
export default {
props: ['program'],
name: "ScoreCard",
data() {
return {
quizData: {}
};
},
mounted() {
this.getQuizProgress().then(data => this.quizData = data)
},
methods: {
getQuizProgress() {
return lms.call('get_quiz_progress_of_program', {
program_name: this.program
})
},
programRoute() {
return {name: 'program', params: {program_name: this.program}}
},
},
components: {
AButton
},
};
</script>

View File

@@ -1,27 +0,0 @@
<template>
<div class="hero">
<h1 class="text-center" v-html="title"></h1>
<p class='text-center' v-html="description"></p>
<p class="text-center padding">
<slot></slot>
</p>
</div>
</template>
<script>
export default {
props: ['title', 'description'],
name: "TopSection",
};
</script>
<style scoped>
.hero {
padding-top: 50px;
padding-bottom: 100px;
}
.hero h1 {
font-size: 40px;
font-weight: 200;
}
</style>

View File

@@ -1,49 +0,0 @@
<template>
<button v-if="isLoggedIn" class='btn btn-primary btn-md' @click="primaryAction()">{{ buttonName }}</button>
<a v-else class='btn btn-primary btn-md' href="/login#signup">{{ buttonName }}</a>
</template>
<script>
export default {
name: "TopSectionButton",
data() {
return {
buttonName: '',
isLoggedIn: lms.store.checkLogin(),
nextContent: '',
nextContentType: '',
nextCourse: '',
link: '',
}
},
mounted() {
this.computeButtons()
},
methods: {
computeButtons(){
if(this.isLoggedIn){
this.buttonName = 'Explore Programs'
}
else{
this.buttonName = 'Sign Up'
}
},
primaryAction() {
if(this.$route.name == 'home'){
this.$router.push('List/Program');
}
else if(this.$route.name == 'program' && lms.store.enrolledPrograms.includes(this.$route.params.program_name)){
this.$router.push({ name: 'content', params: { program_name: this.$route.params.program_name, course: this.nextCourse, type: this.nextContentType, content: this.nextContent}})
}
else {
lms.call("enroll_in_program",
{
program_name: this.$route.params.program_name,
student_email_id: frappe.session.user
}
)
lms.store.updateEnrolledPrograms()
}
},
}
};
</script>

View File

@@ -1,112 +0,0 @@
<template>
<div class="py-3 col-md-4 col-sm-12">
<div class="card h-100">
<div class="card-hero-img" v-if="topic.hero_image" v-bind:style="{ 'background-image': 'url(' + image + ')' }"></div>
<div v-else class="card-image-wrapper">
<div class="image-body">{{ topic.topic_name }}</div>
</div>
<div class='card-body'>
<h5 class="card-title">{{ topic.topic_name }}</h5>
<span class="course-list text-muted" id="getting-started">
Content
<ul class="mb-0 mt-1" style="padding-left: 1.5em;">
<li v-for="content in topic.topic_content" :key="content.name">
<router-link v-if="isLogin" tag="a" :class="'text-muted'" :to="{name: 'content', params:{program_name: program_name, topic:topic.name, course_name: course_name, type:content.content_type, content: content.content} }">
{{ content.content }}
</router-link>
<div v-else><span style="padding-right: 0.4em"></span>{{ content.content }}</div>
</li>
</ul>
</span>
</div>
<div v-if="isLogin" class='p-3' style="display: flex; justify-content: space-between;">
<div>
<span v-if="complete"><i class="mr-2 text-success fa fa-check-circle" aria-hidden="true"></i>Course Complete</span>
</div>
<div class='text-right'>
<a-button
:type="'primary'"
size="sm"
:route="firstContentRoute"
>
{{ buttonName }}
</a-button>
</div>
</div>
</div>
</div>
</template>
<script>
import AButton from './Button.vue';
export default {
props: ['topic', 'course_name', 'program_name'],
name: "TopicCard",
data() {
return {
topicDetails: {}
}
},
mounted() {
if(lms.store.checkLogin()) this.gettopicDetails().then(data => this.topicDetails = data)
},
components: {
AButton
},
computed: {
firstContentRoute() {
if(lms.store.checkLogin()){
return `/Program/${this.program_name}/${this.course_name}/${this.topic.name}/${this.topicDetails.content_type}/${this.topicDetails.content}`
}
else {
return {}
}
},
complete() {
if(lms.store.checkProgramEnrollment(this.program_name)){
if (this.topicDetails.flag === "Completed" ) {
return true
}
else {
return false
}
}
else {
return false
}
},
isLogin() {
// return lms.store.checkProgramEnrollment(this.program_name)
return lms.store.checkLogin()
},
buttonName() {
if(lms.store.checkProgramEnrollment(this.program_name)){
if (this.topicDetails.flag == 'Continue'){
return 'Continue'
}
else {
return 'Start Topic'
}
}
else {
return "Explore"
}
}
},
methods: {
iconClass(content_type) {
if(content_type == 'Video') return 'fa fa-play'
if(content_type == 'Article') return 'fa fa-file-text-o'
if(content_type == 'Quiz') return 'fa fa-question-circle-o'
},
gettopicDetails() {
return lms.call('get_student_topic_details', {
topic_name: this.topic.name,
course_name: this.course_name,
})
},
}
};
</script>

View File

@@ -1,63 +0,0 @@
<template>
<div>
<div class='mt-2'>
<div>
<div class="mt-3 row">
<div class="col-md-8">
<h2>{{ contentData.name }}</h2>
<span class="text-muted">
<i class="octicon octicon-clock" title="Duration"></i> <span v-if="contentData.duration"> {{ contentData.duration }} Mins &mdash; </span><span v-if="contentData.publish_date"> Published on {{ contentData.publish_date }}. </span>
</span>
</div>
<div class="col-md-4 text-right">
<slot></slot>
</div>
</div>
<youtube-player :url="contentData.url" class="mt-3"/>
<hr>
</div>
</div>
<div class="video-description-section">
<div>
<div class="content" v-html="contentData.description">
</div>
<div class="text-right hidden">
<a class='btn btn-outline-secondary' href="/classrooms/module">Previous</a>
<a class='btn btn-primary' href="/classrooms/module">Next</a>
</div>
<div class="mt-3 text-right">
<a class="text-muted" href="/report"><i class="octicon octicon-issue-opened" title="Report"></i> Report a
Mistake</a>
</div>
</div>
</div>
</div>
</template>
<script>
import YoutubePlayer from './YoutubePlayer.vue'
export default {
props: ['content', 'type'],
name: 'Video',
data() {
return {
contentData: '',
}
},
components: {
YoutubePlayer
},
mounted() {
this.getContent()
.then(data => this.contentData = data)
},
methods: {
getContent() {
return lms.call('get_content', {
content_type: this.type,
content: this.content
})
}
}
};
</script>

View File

@@ -1,35 +0,0 @@
<template>
<div class="modal" id="videoModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span id="close_modal" aria-hidden="true" @click="stopVideo()">&times;</span>
</button>
</div>
<div class="modal-body">
<youtube-player :url="video"/>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
import YoutubePlayer from './YoutubePlayer.vue'
export default {
name: 'VideoModal',
props: ['title', 'video'],
components: {
YoutubePlayer
},
methods: {
stopVideo() {
$('.yvideo').each(function() {
this.contentWindow.postMessage('{"event":"command","func":"stopVideo","args":""}', '*')
});
}
}
};
</script>

View File

@@ -1,36 +0,0 @@
<template>
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item yvideo" :src="'https://www.youtube.com/embed/' + videoID + '?version=3&enablejsapi=1'" allowfullscreen></iframe>
</div>
</template>
<script type="text/javascript">
export default {
name: 'YoutubePlayer',
props: ['url'],
data() {
return {
videoID: ''
}
},
watch: {
url() {
this.videoID = this.getVideoID(this.url)
}
},
methods: {
getVideoID(link) {
if (!Array.prototype.last){
Array.prototype.last = function(){
return this[this.length - 1];
};
};
if (link.includes('v=')){
return link.split('v=')[1].split('&')[0]
}
else if (link.includes('youtu.be')) {
return link.split('/').last().split('?')[0]
}
}
}
};
</script>

View File

@@ -1,81 +0,0 @@
import Vue from 'vue/dist/vue.js';
import VueRouter from 'vue-router/dist/vue-router.js';
import moment from 'moment/min/moment.min.js';
import lmsRoot from "./lmsRoot.vue";
import routes from './routes';
import './call';
Vue.use(VueRouter);
var store = {
enrolledPrograms: [],
enrolledCourses: []
};
// let profile_page = `<a class="dropdown-item" href="/lms#/Profile" rel="nofollow"> LMS Profile </a>`
// document.querySelector('#website-post-login > ul').innerHTML += profile_page
frappe.ready(() => {
frappe.provide('lms');
lms.moment = moment;
lms.store = new Vue({
data: store,
methods: {
updateEnrolledPrograms() {
if(this.checkLogin()) {
lms.call("get_program_enrollments").then(data => {
this.enrolledPrograms = data;
});
}
},
updateEnrolledCourses() {
if(this.checkLogin()) {
lms.call("get_all_course_enrollments").then(data => {
this.enrolledCourses = data;
});
}
},
checkLogin() {
return frappe.is_user_logged_in();
},
updateState() {
this.checkLogin();
this.updateEnrolledPrograms();
this.updateEnrolledCourses();
},
checkProgramEnrollment(programName) {
if(this.checkLogin()){
if(this.enrolledPrograms) {
if(this.enrolledPrograms.includes(programName)) {
return true;
}
else {
return false;
}
}
else {
return false;
}
}
else {
return false;
}
}
}
});
lms.view = new Vue({
el: "#lms-app",
router: new VueRouter({ routes }),
template: "<lms-root/>",
components: { lmsRoot },
mounted() {
lms.store.updateState();
}
});
lms.view.$router.afterEach((to, from) => {
window.scrollTo(0,0);
});
});

View File

@@ -1,45 +0,0 @@
<template>
<div id="lms-root">
<navbar></navbar>
<main class="container my-5">
<div class="page_content">
<router-view :key="$route.fullPath"></router-view>
</div>
</main>
</div>
</template>
<script>
import Navbar from "./components/Navbar.vue"
export default {
name: "lmsRoot",
components: {
Navbar
}
};
</script>
<style>
div.card-hero-img {
height: 220px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-color: rgb(250, 251, 252);
}
.card-image-wrapper {
display: flex;
overflow: hidden;
height: 220px;
background-color: rgb(250, 251, 252);
justify-content: center;
}
.image-body {
align-self: center;
color: #d1d8dd;
font-size: 24px;
font-weight: 600;
line-height: 1;
padding: 20px;
}
</style>

View File

@@ -1,84 +0,0 @@
<template>
<div>
<component v-bind:is="currentComponent" :content="content" :type="type">
<ContentNavigation :nextContent="nextContent" :nextContentType="nextContentType"/>
</component>
</div>
</template>
<script>
import Article from "../components/Article.vue"
import Quiz from "../components/Quiz.vue"
import Video from "../components/Video.vue"
import ContentNavigation from "../components/ContentNavigation.vue"
export default {
props:['program_name', 'course_name', 'topic', 'type', 'content'],
name: "ContentPage",
data() {
return{
nextContent: '',
nextContentType: '',
}
},
computed: {
currentComponent: function() {
if(this.type === "Article") {
return 'Article'
}
else if(this.type === "Quiz") {
return 'Quiz'
}
else if(this.type === "Video") {
return 'Video'
}
},
},
mounted() {
this.getNextContent().then(data => {
this.nextContent = data.content,
this.nextContentType = data.content_type
});
},
methods: {
getNextContent(){
return lms.call("get_next_content",
{
current_content: this.content,
current_content_type: this.type,
topic: this.topic,
}
);
}
},
components: {
Article,
Video,
Quiz,
ContentNavigation
}
};
</script>
<style>
.footer-message {
display: none;
}
.video-description-section {
padding-top: 0em !important;
}
.article-top-section {
padding-top: 0.5em !important;
padding-bottom: 0rem !important;
}
.article-content-section {
padding-top: 0em !important;
}
.quiz-section {
padding-top: 0.5em !important;
padding-bottom: 0rem !important;
}
</style>

View File

@@ -1,49 +0,0 @@
<template>
<div>
<breadcrumb></breadcrumb>
<TopSection v-bind:title="course.course_name" v-bind:description="course.course_intro">
</TopSection>
<CardList :title="'Topics'" :description="''">
<TopicCard slot="card-list-slot" v-for="topic in topicData" :topic="topic" :course_name="course_name" :program_name="program_name" :key="topic.name"/>
</CardList>
</div>
</template>
<script>
import TopSection from "../components/TopSection.vue"
import CardList from "../components/CardList.vue"
import TopicCard from "../components/TopicCard.vue"
import Breadcrumb from "../components/Breadcrumb.vue"
export default {
props: ['program_name','course_name'],
name: "CoursePage",
components: {
TopSection,
CardList,
TopicCard,
Breadcrumb
},
data() {
return {
course: {},
topicData: [],
}
},
mounted() {
this.getCourseDetails().then(data => this.course = data);
this.getTopics().then(data => this.topicData = data);
},
methods: {
getCourseDetails() {
return lms.call('get_course_details', {
course_name: this.course_name
});
},
getTopics() {
return lms.call('get_topics', {
course_name: this.course_name
})
}
}
};
</script>

View File

@@ -1,48 +0,0 @@
<template>
<div>
<TopSection :title="portal.title" :description="portal.description">
<TopSectionButton/>
</TopSection>
<CardList :title="'Featured Programs'" :description="'Master ERPNext'">
<ProgramCard slot="card-list-slot" v-for="item in featuredPrograms" :key="item.program.name" :program="item.program" :enrolled="item.is_enrolled"/>
<AButton slot="list-bottom" :type="'primary'" :size="'md'" :route="'List/Program'">View All</AButton>
</CardList>
</div>
</template>
<script>
import Button from '../components/Button.vue';
import TopSection from "../components/TopSection.vue"
import CardList from "../components/CardList.vue"
import ProgramCard from "../components/ProgramCard.vue"
import TopSectionButton from "../components/TopSectionButton.vue"
export default {
name: "Home",
data() {
return{
portal: {},
featuredPrograms: {},
// enrolledPrograms: new Set()
}
},
components: {
AButton: Button,
TopSection,
CardList,
ProgramCard,
TopSectionButton
},
mounted() {
this.getPortalDetails().then(data => this.portal = data);
this.getFeaturedPrograms().then(data => this.featuredPrograms = data);
},
methods: {
getPortalDetails() {
return lms.call("get_portal_details")
},
getFeaturedPrograms() {
return lms.call("get_featured_programs")
}
}
};
</script>

View File

@@ -1,53 +0,0 @@
<template>
<div>
<TopSection :title="'Programs at ' + portal.title" :description="portal.description">
<AButton v-if="isLogin" :type="'primary'" :size="'lg'" :route="{ name: 'signup'}">Sign Up</AButton>
</TopSection>
<CardList :title="'All Programs'" :description="''">
<ProgramCard slot="card-list-slot" v-for="item in masterData" :key="item.program.name" :program="item.program" :enrolled="item.is_enrolled"/>
</CardList>
</div>
</template>
<script>
import ProgramCard from '../components/ProgramCard.vue';
import CourseCard from "../components/CourseCard.vue"
import Button from '../components/Button.vue';
import TopSection from "../components/TopSection.vue"
import CardList from "../components/CardList.vue"
export default {
props: ['master'],
name: "ListPage",
components: {
AButton: Button,
CourseCard,
ProgramCard,
CardList,
TopSection
},
data() {
return {
portal: {},
masterData: {}
}
},
mounted() {
this.getPortalDetails().then(data => this.portal = data);
this.getMaster().then(data => this.masterData = data);
},
methods: {
getPortalDetails() {
return lms.call("get_portal_details")
},
getMaster() {
return lms.call("get_all_programs")
}
},
computed: {
isLogin() {
return !lms.store.checkLogin()
}
}
};
</script>

View File

@@ -1,50 +0,0 @@
<template>
<div>
<ProfileInfo :enrolledPrograms="enrolledPrograms"></ProfileInfo>
<div v-if="enrolledPrograms">
<CardList :title="'Your Progress'" :description="''">
<ProgressCard slot="card-list-slot" v-for="program in enrolledPrograms" :program="program" :key="program"/>
</CardList>
<CardList :title="''" :description="''">
<ScoreCard slot="card-list-slot" v-for="program in enrolledPrograms" :program="program" :key="program"/>
</CardList>
</div>
<div v-else>
You haven't enrolled in any programs yet.
</div>
</div>
</template>
<script>
import Button from '../components/Button.vue';
import TopSection from "../components/TopSection.vue"
import CardList from "../components/CardList.vue"
import ProgressCard from "../components/ProgressCard.vue"
import ProfileInfo from "../components/ProfileInfo.vue"
import ScoreCard from "../components/ScoreCard.vue"
export default {
name: "ProfilePage",
components: {
AButton: Button,
TopSection,
CardList,
ProfileInfo,
ProgressCard,
ScoreCard
},
data() {
return {
enrolledPrograms: {},
}
},
mounted() {
this.getEnrolledPrograms().then(data => this.enrolledPrograms = data);
},
methods: {
getEnrolledPrograms() {
return lms.call("get_program_enrollments")
}
}
};
</script>

View File

@@ -1,49 +0,0 @@
<template>
<div>
<breadcrumb></breadcrumb>
<TopSection v-bind:title="program.program_name" v-bind:description="program.description">
</TopSection>
<CardList :title="'Courses'" :description="''">
<CourseCard slot="card-list-slot" v-for="course in courseData" :course="course" :program_name="program_name" :key="course.name"/>
</CardList>
</div>
</template>
<script>
import TopSection from "../components/TopSection.vue"
import CardList from "../components/CardList.vue"
import CourseCard from "../components/CourseCard.vue"
import Breadcrumb from "../components/Breadcrumb.vue"
export default {
props: ['program_name'],
name: "ProgramPage",
components: {
TopSection,
CardList,
CourseCard,
Breadcrumb
},
data() {
return {
program: {},
courseData: [],
}
},
mounted() {
this.getProgramDetails().then(data => this.program = data);
this.getCourses().then(data => this.courseData = data);
},
methods: {
getProgramDetails() {
return lms.call('get_program', {
program_name: this.program_name
});
},
getCourses() {
return lms.call('get_courses', {
program_name: this.program_name
})
}
}
};
</script>

View File

@@ -0,0 +1,186 @@
class Quiz {
constructor(wrapper, options) {
this.wrapper = wrapper;
Object.assign(this, options);
this.questions = []
this.refresh();
}
refresh() {
this.get_quiz();
}
get_quiz() {
frappe.call('erpnext.education.utils.get_quiz', {
quiz_name: this.name,
course: this.course
}).then(res => {
this.make(res.message)
});
}
make(data) {
data.questions.forEach(question_data => {
let question_wrapper = document.createElement('div');
let question = new Question({
wrapper: question_wrapper,
...question_data
});
this.questions.push(question)
this.wrapper.appendChild(question_wrapper);
})
if (data.activity.is_complete) {
this.disable()
let indicator = 'red'
let message = 'Your are not allowed to attempt the quiz again.'
if (data.activity.result == 'Pass') {
indicator = 'green'
message = 'You have already cleared the quiz.'
}
this.set_quiz_footer(message, indicator, data.activity.score)
}
else {
this.make_actions();
}
}
make_actions() {
const button = document.createElement("button");
button.classList.add("btn", "btn-primary", "mt-5", "mr-2");
button.id = 'submit-button';
button.innerText = 'Submit';
button.onclick = () => this.submit();
this.submit_btn = button
this.wrapper.appendChild(button);
}
submit() {
this.submit_btn.innerText = 'Evaluating..'
this.submit_btn.disabled = true
this.disable()
frappe.call('erpnext.education.utils.evaluate_quiz', {
quiz_name: this.name,
quiz_response: this.get_selected(),
course: this.course,
program: this.program
}).then(res => {
this.submit_btn.remove()
if (!res.message) {
frappe.throw("Something went wrong while evaluating the quiz.")
}
let indicator = 'red'
let message = 'Fail'
if (res.message.status == 'Pass') {
indicator = 'green'
message = 'Congratulations, you cleared the quiz.'
}
this.set_quiz_footer(message, indicator, res.message.score)
});
}
set_quiz_footer(message, indicator, score) {
const div = document.createElement("div");
div.classList.add("mt-5");
div.innerHTML = `<div class="row">
<div class="col-md-8">
<h4>${message}</h4>
<h5 class="text-muted"><span class="indicator ${indicator}">Score: ${score}/100</span></h5>
</div>
<div class="col-md-4">
<a href="${this.next_url}" class="btn btn-primary pull-right">${this.quiz_exit_button}</a>
</div>
</div>`
this.wrapper.appendChild(div)
}
disable() {
this.questions.forEach(que => que.disable())
}
get_selected() {
let que = {}
this.questions.forEach(question => {
que[question.name] = question.get_selected()
})
return que
}
}
class Question {
constructor(opts) {
Object.assign(this, opts);
this.make();
}
make() {
this.make_question()
this.make_options()
}
get_selected() {
let selected = this.options.filter(opt => opt.input.checked)
if (this.type == 'Single Correct Answer') {
if (selected[0]) return selected[0].name
}
if (this.type == 'Multiple Correct Answer') {
return selected.map(opt => opt.name)
}
return null
}
disable() {
let selected = this.options.forEach(opt => opt.input.disabled = true)
}
make_question() {
let question_wrapper = document.createElement('h5');
question_wrapper.classList.add('mt-3');
question_wrapper.innerText = this.question;
this.wrapper.appendChild(question_wrapper);
}
make_options() {
let make_input = (name, value) => {
let input = document.createElement('input');
input.id = name;
input.name = this.name;
input.value = value;
input.type = 'radio';
if (this.type == 'Multiple Correct Answer')
input.type = 'checkbox';
input.classList.add('form-check-input');
return input;
}
let make_label = function(name, value) {
let label = document.createElement('label');
label.classList.add('form-check-label');
label.htmlFor = name;
label.innerText = value;
return label
}
let make_option = function (wrapper, option) {
let option_div = document.createElement('div')
option_div.classList.add('form-check', 'pb-1')
let input = make_input(option.name, option.option);
let label = make_label(option.name, option.option);
option_div.appendChild(input)
option_div.appendChild(label)
wrapper.appendChild(option_div)
return {input: input, ...option}
}
let options_wrapper = document.createElement('div')
options_wrapper.classList.add('ml-2')
let option_list = []
this.options.forEach(opt => option_list.push(make_option(options_wrapper, opt)))
this.options = option_list
this.wrapper.appendChild(options_wrapper)
}
}

View File

@@ -1,92 +0,0 @@
import Home from "./pages/Home.vue";
import ProgramPage from "./pages/ProgramPage.vue";
import CoursePage from "./pages/CoursePage.vue";
import ContentPage from "./pages/ContentPage.vue";
import ListPage from "./pages/ListPage.vue";
import ProfilePage from "./pages/ProfilePage.vue";
const routes = [{
name: 'home',
path: '',
component: Home
},
{
name: 'program',
path: '/Program/:program_name',
component: ProgramPage,
props: true
},
{
name: 'course',
path: '/Program/:program_name/:course_name/',
component: CoursePage,
props: true,
},
{
name: 'content',
path: '/Program/:program_name/:course_name/:topic/:type/:content',
component: ContentPage,
props: true,
beforeRouteUpdate (to, from, next) {
if (lms.store.checkProgramEnrollment(to.params.program_name)) {
next();
} else {
next({
name: 'program',
params: {
program_name: to.params.program_name
}
});
}
}
},
{
name: 'list',
path: '/List/:master',
component: ListPage,
props: true
},
{
name: 'signup',
path: '/Signup',
beforeEnter(to, from, next) {
window.location = window.location.origin.toString() + '/login#signup';
},
component: Home,
props: true
},
{
name: 'login',
path: '/Login',
beforeEnter(to, from, next) {
window.location = window.location.origin.toString() + '/login#login';
},
component: Home,
props: true
},
{
name: 'logout',
path: '/Logout',
beforeEnter(to, from, next) {
window.location = window.location.origin.toString() + '/?cmd=web_logout';
},
component: Home,
props: true
},
{
name: 'profile',
path: '/Profile',
component: ProfilePage,
props: true,
beforeEnter: (to, from, next) => {
if (!lms.store.checkLogin()) {
next({
name: 'home'
});
} else {
next();
}
}
}];
export default routes;

View File

@@ -63,7 +63,7 @@ erpnext.financial_statements = {
};
function get_filters(){
return [
let filters = [
{
"fieldname":"company",
"label": __("Company"),
@@ -78,37 +78,6 @@ function get_filters(){
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"label": __("Cost Center"),
"fieldtype": "MultiSelect",
get_data: function() {
var cost_centers = frappe.query_report.get_filter_value("cost_center") || "";
const values = cost_centers.split(/\s*,\s*/).filter(d => d);
const txt = cost_centers.match(/[^,\s*]*$/)[0] || '';
let data = [];
frappe.call({
type: "GET",
method:'frappe.desk.search.search_link',
async: false,
no_spinner: true,
args: {
doctype: "Cost Center",
txt: txt,
filters: {
"company": frappe.query_report.get_filter_value("company"),
"name": ["not in", values]
}
},
callback: function(r) {
data = r.results;
}
});
return data;
}
},
{
"fieldname":"from_fiscal_year",
"label": __("Start Year"),
@@ -147,6 +116,54 @@ function get_filters(){
"label": __("Currency"),
"fieldtype": "Select",
"options": erpnext.get_presentation_currency_list()
},
{
"fieldname":"cost_center",
"label": __("Cost Center"),
"fieldtype": "MultiSelect",
get_data: function() {
var cost_centers = frappe.query_report.get_filter_value("cost_center") || "";
const values = cost_centers.split(/\s*,\s*/).filter(d => d);
const txt = cost_centers.match(/[^,\s*]*$/)[0] || '';
let data = [];
frappe.call({
type: "GET",
method:'frappe.desk.search.search_link',
async: false,
no_spinner: true,
args: {
doctype: "Cost Center",
txt: txt,
filters: {
"company": frappe.query_report.get_filter_value("company"),
"name": ["not in", values]
}
},
callback: function(r) {
data = r.results;
}
});
return data;
}
}
]
let dimension_filters = erpnext.get_dimension_filters();
dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
filters.push({
"fieldname": dimension["fieldname"],
"label": __(dimension["label"]),
"fieldtype": "Link",
"options": dimension["document_type"]
});
});
});
return filters;
}

View File

@@ -62,6 +62,17 @@ $.extend(erpnext, {
$btn.on("click", function() {
me.show_serial_batch_selector(grid_row.frm, grid_row.doc);
});
},
get_dimension_filters: async function() {
let dimensions = await frappe.db.get_list('Accounting Dimension', {
fields: ['label', 'fieldname', 'document_type'],
filters: {
disabled: 0
}
});
return dimensions;
}
});

View File

@@ -0,0 +1,24 @@
frappe.provide('frappe.ui.form');
erpnext.doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
"Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Sales Invoice Item", "Purchase Invoice Item",
"Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item",
"Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile"];
let dimension_filters = erpnext.get_dimension_filters();
erpnext.doctypes_with_dimensions.forEach((doctype) => {
frappe.ui.form.on(doctype, {
onload: function(frm) {
dimension_filters.then((dimensions) => {
dimensions.forEach((dimension) => {
frm.set_query(dimension['fieldname'],{
"is_group": 0
});
});
});
}
});
});

View File

@@ -373,13 +373,24 @@ erpnext.SerialNoBatchSelector = Class.extend({
get_serial_no_fields: function() {
var me = this;
this.serial_list = [];
let serial_no_filters = {
item_code: me.item_code,
delivery_document_no: ""
}
if (me.warehouse_details.name) {
serial_no_filters['warehouse'] = me.warehouse_details.name;
}
return [
{fieldtype: 'Section Break', label: __('Serial Numbers')},
{
fieldtype: 'Link', fieldname: 'serial_no_select', options: 'Serial No',
label: __('Select to add Serial Number.'),
get_query: function() {
return { filters: {item_code: me.item_code, warehouse: me.warehouse_details.name}};
return {
filters: serial_no_filters
};
},
onchange: function(e) {
if(this.in_local_change) return;