From e2b0b843f86e1e42f868266af184a50e707e4a59 Mon Sep 17 00:00:00 2001
From: Artem Konenko <yadummer@gmail.com>
Date: Sun, 24 Sep 2017 20:54:05 +0300
Subject: [PATCH] Add a few models for plans, record books and so on.

---
 db/migrations/stored/R__functions.sql         | 191 +++++++++++++++++-
 .../stored/R__stored_subdivisions.sql         |  14 +-
 .../structure/V2_0_4_6__upd_record_books.sql  |  19 ++
 .../classes/Controller/Api/V0/Student.php     | 186 +++++++++++++++--
 .../classes/Controller/Api/V0/StudyPlan.php   | 147 ++++++++++++++
 .../classes/Controller/Api/V0/Teacher.php     |  30 +--
 .../classes/Controller/Handler/Api.php        |  39 ++++
 .../application/classes/Model/Discipline.php  |   8 +
 .../application/classes/Model/Faculties.php   |  12 ++
 .../Model/Helper/DisciplineBuilder.php        |   3 +
 .../classes/Model/Helper/PlanBuilder.php      |  17 ++
 .../Model/Helper/RecordBookBuilder.php        |  44 ++++
 .../application/classes/Model/Plan.php        |  61 ++++++
 .../application/classes/Model/RecordBook.php  | 135 +++++++++++++
 .../application/classes/Model/Student.php     |  10 +-
 .../application/classes/Model/Subject.php     |   9 +-
 .../application/classes/Model/Teacher.php     |   8 +
 ~dev_rating/application/routes/api/v0.php     |  13 ++
 .../application/views/office/sync.twig        |   5 +-
 19 files changed, 890 insertions(+), 61 deletions(-)
 create mode 100644 db/migrations/structure/V2_0_4_6__upd_record_books.sql
 create mode 100644 ~dev_rating/application/classes/Controller/Api/V0/StudyPlan.php
 create mode 100644 ~dev_rating/application/classes/Model/Helper/PlanBuilder.php
 create mode 100644 ~dev_rating/application/classes/Model/Helper/RecordBookBuilder.php
 create mode 100644 ~dev_rating/application/classes/Model/Plan.php
 create mode 100644 ~dev_rating/application/classes/Model/RecordBook.php

diff --git a/db/migrations/stored/R__functions.sql b/db/migrations/stored/R__functions.sql
index 028903bfb..7ad43ef6f 100644
--- a/db/migrations/stored/R__functions.sql
+++ b/db/migrations/stored/R__functions.sql
@@ -289,6 +289,23 @@ BEGIN
     RETURN 0; # Успешно удалено;
 END //
 
+-- Получение внутреннего ID по коду справочника из 1С
+DROP FUNCTION IF EXISTS Subject_GetIDFromExternalID//
+CREATE FUNCTION Subject_GetIDFromExternalID (
+    pSubjectExternalID VARCHAR(9)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT subjects.ID INTO pID
+        FROM subjects
+        WHERE subjects.ExternalID = pSubjectExternalID
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
 # -------------------------------------------------------------------------------------------
 # Label: accounts
 # -------------------------------------------------------------------------------------------
@@ -424,6 +441,40 @@ BEGIN
     RETURN 1;
 END //
 
+-- Получение внутреннего ID по хешу СНИЛС из 1С
+DROP FUNCTION IF EXISTS Account_GetIDFromINILA//
+CREATE FUNCTION Account_GetIDFromINILA (
+    pAccountINILA VARCHAR(9)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT accounts.ID INTO pID
+        FROM accounts
+        WHERE accounts.INILA = pAccountINILA
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
+-- Получение внутреннего ID по коду справочника из 1С
+DROP FUNCTION IF EXISTS Account_GetIDFromExternalID//
+CREATE FUNCTION Account_GetIDFromExternalID (
+    pAccountExternalID VARCHAR(9)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT accounts.ID INTO pID
+        FROM accounts
+        WHERE accounts.ExternalID = pAccountExternalID
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
 # -------------------------------------------------------------------------------------------
 # Label: teachers
 # -------------------------------------------------------------------------------------------
@@ -544,7 +595,7 @@ END //
 -- Получение внутреннего ID по коду справочника из 1С
 DROP FUNCTION IF EXISTS Teacher_GetIDFromExternalID//
 CREATE FUNCTION Teacher_GetIDFromExternalID (
-    pTeacherExternalID INT
+    pTeacherExternalID VARCHAR(9)
 ) RETURNS int(11)
 READS SQL DATA
     BEGIN
@@ -559,6 +610,24 @@ READS SQL DATA
         RETURN pID;
     END //
 
+-- Получение внутреннего ID по хешу СНИЛС из 1С
+DROP FUNCTION IF EXISTS Teacher_GetIDFromINILA//
+CREATE FUNCTION Teacher_GetIDFromINILA (
+    pTeacherINILA VARCHAR(40)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT teachers.ID INTO pID
+        FROM teachers
+            INNER JOIN accounts ON teachers.AccountID = accounts.ID
+        WHERE accounts.INILA = pTeacherINILA
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
 # -------------------------------------------------------------------------------------------
 # Label: students
 # -------------------------------------------------------------------------------------------
@@ -641,6 +710,24 @@ BEGIN
     RETURN CreateStudent(pLastName, pFirstName, pSecondName, vGroupID, pActivationCode, pSemesterID);
 END //
 
+-- Получение внутреннего ID по коду справочника из 1С
+DROP FUNCTION IF EXISTS Student_GetIDFromExternalID//
+CREATE FUNCTION Student_GetIDFromExternalID (
+    pAccountExternalID VARCHAR(9)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT students.ID INTO pID
+        FROM accounts
+        JOIN students ON accounts.ID = students.AccountID
+        WHERE accounts.ExternalID = pAccountExternalID
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
 # Give a student an academic leave or attach him to group.
 #   params:
 #      StudentID (int)
@@ -688,6 +775,91 @@ BEGIN
     RETURN ROW_COUNT()-1;
 END //
 
+# -------------------------------------------------------------------------------------------
+# Label: record books
+# -------------------------------------------------------------------------------------------
+
+DROP FUNCTION IF EXISTS RecordBook_Create//
+CREATE FUNCTION RecordBook_Create (
+    pExternalID VARCHAR(30) CHARSET utf8,
+    pStudentID  VARCHAR(30) CHARSET utf8,
+    pPlanID     VARCHAR(30) CHARSET utf8,
+    pFacultyID  INT
+)   RETURNS int(11)
+NO SQL
+    BEGIN
+        DECLARE vRecordBookID INT DEFAULT -1;
+        DECLARE EXIT HANDLER FOR SQLEXCEPTION RETURN -1;
+
+        INSERT INTO record_books (StudentID, ExternalID, PlanID)
+        VALUES  (pStudentID, pExternalID, pPlanID);
+        SET vRecordBookID = LAST_INSERT_ID();
+
+        RETURN vRecordBookID;
+    END //
+
+-- Получение внутреннего ID по коду справочника из 1С
+DROP FUNCTION IF EXISTS RecordBook_GetIDFromExternalID//
+CREATE FUNCTION RecordBook_GetIDFromExternalID (
+    pRecordBookExternalID VARCHAR(20)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT record_books.ID INTO pID
+        FROM record_books
+        WHERE record_books.ExternalID = pRecordBookExternalID
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
+# -------------------------------------------------------------------------------------------
+# Label: plans
+# -------------------------------------------------------------------------------------------
+
+-- Получение внутреннего ID по коду справочника из 1С
+DROP FUNCTION IF EXISTS Plan_GetIDFromExternalID//
+CREATE FUNCTION Plan_GetIDFromExternalID (
+    pPlanExternalID VARCHAR(9)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT study_plans.ID INTO pID
+        FROM study_plans
+        WHERE study_plans.ExternalID = pPlanExternalID
+        LIMIT 1;
+
+        IF pID = -1 THEN
+            INSERT INTO study_plans (ExternalID)
+            VALUES (pPlanExternalID);
+            SET pID = LAST_INSERT_ID();
+        END IF;
+
+        RETURN pID;
+    END //
+
+DROP PROCEDURE IF EXISTS Plan_GetInfo //
+CREATE PROCEDURE Plan_GetInfo(IN pID INT(11))
+READS SQL DATA
+    BEGIN
+        SELECT
+            study_plans.ID,
+            study_plans.ExternalID,
+            study_groups.FacultyID,
+            students_groups.SemesterID,
+            study_groups.GradeID
+        FROM study_plans
+        JOIN record_books ON record_books.PlanID = study_plans.ID
+        JOIN students_groups ON record_books.ID = students_groups.RecordBookID
+        JOIN study_groups ON students_groups.GroupID = study_groups.ID
+        WHERE pID = study_plans.ID
+        LIMIT 1;
+    END //
+
 # -------------------------------------------------------------------------------------------
 # Label: disciplines
 # -------------------------------------------------------------------------------------------
@@ -762,6 +934,23 @@ BEGIN
     RETURN vDisciplineID;
 END //
 
+-- Получение внутреннего ID по коду справочника из 1С
+DROP FUNCTION IF EXISTS Discipline_GetIDFromExternalID//
+CREATE FUNCTION Discipline_GetIDFromExternalID (
+    pDisciplineExternalID VARCHAR(9)
+) RETURNS int(11)
+READS SQL DATA
+    BEGIN
+        DECLARE pID INT DEFAULT -1;
+
+        SELECT disciplines.ID INTO pID
+        FROM disciplines
+        WHERE disciplines.ExternalID = pDisciplineExternalID
+        LIMIT 1;
+
+        RETURN pID;
+    END //
+
 DROP FUNCTION IF EXISTS ChangeDisciplineSubjectUnsafe//
 CREATE FUNCTION ChangeDisciplineSubjectUnsafe (
     pDisciplineID INT,
diff --git a/db/migrations/stored/R__stored_subdivisions.sql b/db/migrations/stored/R__stored_subdivisions.sql
index 4d6ac63d8..800334568 100644
--- a/db/migrations/stored/R__stored_subdivisions.sql
+++ b/db/migrations/stored/R__stored_subdivisions.sql
@@ -59,16 +59,14 @@ BEGIN
   RETURN pFacultyID;
 END//
 
-DROP FUNCTION IF EXISTS Faculty_GetIdByExternalID//
-CREATE FUNCTION Faculty_GetIdByExternalID(
-  pFacultyExternalID VARCHAR(9)
-) RETURNS INT(11) # -1 or id
+-- Получение внутреннего ID по коду справочника из 1С
+DROP PROCEDURE IF EXISTS Faculty_GetIdByExternalID//
+CREATE PROCEDURE Faculty_GetIdByExternalID(IN pFacultyExternalID VARCHAR(9))
 READS SQL DATA
 BEGIN
-  RETURN (SELECT faculties.ID as ID
-          FROM faculties
-          WHERE faculties.ExternalID = pFacultyExternalID
-          LIMIT 1);
+  SELECT faculties.ID as ID
+  FROM faculties
+  WHERE faculties.ExternalID = pFacultyExternalID;
 END//
 
 # -------------------------------------------------------------------------------------------
diff --git a/db/migrations/structure/V2_0_4_6__upd_record_books.sql b/db/migrations/structure/V2_0_4_6__upd_record_books.sql
new file mode 100644
index 000000000..771d9f2a9
--- /dev/null
+++ b/db/migrations/structure/V2_0_4_6__upd_record_books.sql
@@ -0,0 +1,19 @@
+START TRANSACTION;
+
+CREATE TABLE `disciplines_study_plans` (
+  `ID` int(11) NOT NULL AUTO_INCREMENT,
+  `DisciplineID` int(11) NOT NULL,
+  `StudyPlanID` int(11) NOT NULL,
+  PRIMARY KEY (`ID`),
+  UNIQUE KEY `UniqBind` (`DisciplineID`, `StudyPlanID`)
+) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
+
+ALTER TABLE `disciplines`
+-- Код справочника из 1С
+  ADD COLUMN `ExternalID` VARCHAR(9) CHARACTER SET utf8 DEFAULT NULL AFTER `ID`
+;
+
+ALTER TABLE `accounts`
+  MODIFY COLUMN `INILA` VARCHAR(40) CHARACTER SET utf8 DEFAULT NULL;
+
+COMMIT ;
\ No newline at end of file
diff --git a/~dev_rating/application/classes/Controller/Api/V0/Student.php b/~dev_rating/application/classes/Controller/Api/V0/Student.php
index c04004966..210be72a6 100644
--- a/~dev_rating/application/classes/Controller/Api/V0/Student.php
+++ b/~dev_rating/application/classes/Controller/Api/V0/Student.php
@@ -2,6 +2,150 @@
 
 class Controller_Api_V0_Student extends Controller_Handler_Api
 {
+    private function normalizeRecordBookData($data)
+    {
+        /*
+         * $data = [ +'externalID' => $this->request->query('id')
+                     + 'planExternalID' => $this->request->query('planId')
+                    , 'subdivisionExternalID' => $this->request->query('subdivision')
+                    , 'status' => $this->request->query('status')
+                    , 'level' => $this->request->query('level')
+                    , 'form' => $this->request->query('form')
+                    , 'speciality' => $this->request->query('speciality')
+                    , 'grade' => $this->request->query('grade')
+                    , 'group' => $this->request->query('group')
+                ];
+         */
+        if ( !isset($data->id) && isset($data->externalID) ) {
+            $ID = Model_RecordBook::withExternalID($data->externalID);
+            $data->id = $ID;
+        }
+
+        if ( !isset($data->studentID) ) {
+            if ( isset($data->studentExternalID) ) {
+                $studentID = Model_Student::withExternalID($data->studentExternalID);
+                if ($studentID === -1) {
+                    $this->badRequestError( $this->makeErrorMsg($data, 'Не найден студент по данному идентификатору.'));
+                }
+
+                $data->studentID = $studentID;
+            } else {
+                $this->badRequestError( $this->makeErrorMsg($data, 'Не указан идентификатор студента.'));
+            }
+        }
+
+        if ( !isset($data->planID) ) {
+            if ( isset($data->planExternalID) ) {
+                $planID = Model_Plan::withExternalID($data->planExternalID);
+
+                $data->planID = $planID;
+            } else {
+                $this->badRequestError( $this->makeErrorMsg($data, 'Не указан идентификатор учебного плана.'));
+            }
+        }
+
+        if ( !isset($data->facultyID) ) {
+            if (  isset($data->facultyExternalID) ) {
+                $depid = $this->getFacultyIDByExternalID($data->facultyExternalID);
+                if ( $depid === NULL ) {
+                    $this->badRequestError( $this->makeErrorMsg($data, 'Ошибка в коде справочника факультета.') );
+                }
+                $data->facultyID = $depid;
+            } else {
+                $this->badRequestError( $this->makeErrorMsg($data, 'Не указано подразделение.'));
+            }
+        }
+
+//print_r($data);
+//        echo "\n\n";
+
+        return $data;
+    }
+
+    private function normalizeStudentData($data) {
+        if ( !isset($data->id) && isset($data->externalID) ) {
+            $ID = Model_Student::withExternalID($data->externalID);
+            $data->id = $ID;
+        }
+
+        if ( !isset($data->departmentID) ) {
+            if (  isset($data->departmentExternalID) ) {
+                $depid = $this->getDepIDByExternalID($data->departmentExternalID);
+                if ( $depid === NULL ) {
+                    $this->badRequestError( $this->makeErrorMsg($data, 'Ошибка коде справочника подразделения.') );
+                }
+                $data->departmentID = $depid;
+            } elseif (  isset($data->departmentName) ) {
+                $depid = $this->getDepIDByName($data->departmentName);
+                if ( $depid === NULL ) {
+                    $this->badRequestError( $this->makeErrorMsg($data, 'Ошибка в имени подразделения.') );
+                }
+                $data->departmentID = $depid;
+            } else {
+                $this->badRequestError( $this->makeErrorMsg($data, 'Не указано подразделение.'));
+            }
+        }
+
+        echo $data->firstName.' '.$data->secondName.' '.$data->lastName.' $data->id: '.$data->id."\n";
+        if ( !isset($data->id) || ($data->id == -1) ) {
+            echo "-- Try to find by name !\n";
+            $foundedList = Model_Teachers::search([$data->firstName, $data->secondName, $data->lastName], 0, 0);
+            if ( count($foundedList) === 1 ) {
+                echo '--- Founded by name id: '.$foundedList[0]['ID']."!\n";
+                $data->id = $foundedList[0]['ID'];
+            }
+        }
+
+        if ( !isset($data->jobPositionID) && isset($data->jobPositionName) ) {
+            $jpID = Model_Faculties::getJobPositionIdByName($data->jobPositionName);
+            if ( $jpID === NULL ) {
+                $jpID = Model_Faculties::createJobPosition($data->jobPositionName);
+            }
+            $data->jobPositionID = $jpID;
+        } elseif( !isset($data->jobPositionName) ) {
+            $this->badRequestError( $this->makeErrorMsg($data, 'Не указана должность.') );
+        }
+
+        switch ($data->status) {
+            case 'Работает':
+                $data->status = true;
+                break;
+            case 'Уволен':
+                $data->status = false;
+                break;
+            default:
+                $this->badRequestError( $this->makeErrorMsg($data, 'Указан некорректный статус.') );
+        }
+
+        return $data;
+    }
+
+    private function createRecordBook($data) {
+        try {
+            $recordbook = Model_RecordBook::make()
+                ->studentID($data->studentID)
+                ->planID($data->planID)
+                ->facultyID($data->facultyID);
+
+            if ( isset($data->externalID) ) {
+                $recordbook = $recordbook->externalID($data->externalID);
+            }
+
+            $recordbook = $recordbook->create();
+        } catch (InvalidArgumentException $e) {
+            $this->badRequestError( $this->makeErrorMsg($data, $e->getMessage()) );
+        }
+        return ['ID' => $recordbook->ID];
+    }
+
+    private function updateRecordBook($data) {
+        try {
+//            Model_RecordBook::with($data->id)->changeInfo();
+        } catch (InvalidArgumentException $e) {
+            $this->badRequestError( $this->makeErrorMsg($data, $e->getMessage()) );
+        }
+    }
+
     public function action_get_index() {
         $this->response->body( '{ what: get index }' );
 
@@ -9,10 +153,10 @@ class Controller_Api_V0_Student extends Controller_Handler_Api
     }
 
     /**
-     * @api {put} api/v0/student Add new student(s)
-     * @apiName Add new student
-     * @apiGroup Student
-     * @apiVersion 0.1.0
+     * @api {put} api/v0/student/recordBook Add new record book(s)
+     * @apiName Add new record book
+     * @apiGroup Record books
+     * @apiVersion 0.1.2
      * @apiParam {String} token Api key
      * @apiParam {String} [batch] Flag about massive addition
      * @apiParam {String} firstName
@@ -31,21 +175,23 @@ class Controller_Api_V0_Student extends Controller_Handler_Api
 
                 $res = [];
                 foreach ($data as $item) {
-                    $data = $this->normalizeTeacherData($item);
-                    $res[] = $this->createTeacher($data);
+                    $data = $this->normalizeRecordBookData($item);
+                    $res[] = $this->createRecordBook($data);
                 }
             } else {
-                $data = [ 'firstName' => $this->request->query('firstName')
-                    , 'secondName' => $this->request->query('secondName')
-                    , 'lastName' => $this->request->query('lastName')
-                    , 'departmentName' => $this->request->query('departmentName')
-                    , 'departmentID' => $this->request->query('departmentID')
-                    , 'jobPositionName' => $this->request->query('jobPositionName')
-                    , 'jobPositionID' => $this->request->query('jobPositionID')
-                    , 'status' => $this->request->query('status')];
+                $data = [ 'externalID' => $this->request->query('id')
+                    , 'planExternalID' => $this->request->query('planId')
+                    , 'subdivisionExternalID' => $this->request->query('subdivision')
+                    , 'status' => $this->request->query('status')
+                    , 'level' => $this->request->query('level')
+                    , 'form' => $this->request->query('form')
+                    , 'speciality' => $this->request->query('speciality')
+                    , 'grade' => $this->request->query('grade')
+                    , 'group' => $this->request->query('group')
+                ];
 
-                $data = $this->normalizeTeacherData((object)$data);
-                $res[] = $this->createTeacher($data);
+                $data = $this->normalizeRecordBookData((object)$data);
+                $res[] = $this->createRecordBook($data);
             }
         } catch (Exception $e) {
             $this->badRequestError($e->getMessage());
@@ -77,8 +223,8 @@ class Controller_Api_V0_Student extends Controller_Handler_Api
 
                 $res = [];
                 foreach ($data as $item) {
-                    $data = $this->normalizeTeacherData($item);
-                    $res[] = $this->createTeacher($data);
+                    $data = $this->normalizeStudentData($item);
+                    $res[] = $this->createRecordBook($data);
                 }
             } else {
                 $data = [ 'firstName' => $this->request->query('firstName')
@@ -90,8 +236,8 @@ class Controller_Api_V0_Student extends Controller_Handler_Api
                     , 'jobPositionID' => $this->request->query('jobPositionID')
                     , 'status' => $this->request->query('status')];
 
-                $data = $this->normalizeTeacherData((object)$data);
-                $res[] = $this->createTeacher($data);
+                $data = $this->normalizeStudentData((object)$data);
+                $res[] = $this->createRecordBook($data);
             }
         } catch (Exception $e) {
             $this->badRequestError($e->getMessage());
diff --git a/~dev_rating/application/classes/Controller/Api/V0/StudyPlan.php b/~dev_rating/application/classes/Controller/Api/V0/StudyPlan.php
new file mode 100644
index 000000000..10cb348ba
--- /dev/null
+++ b/~dev_rating/application/classes/Controller/Api/V0/StudyPlan.php
@@ -0,0 +1,147 @@
+<?php
+
+class Controller_Api_V0_StudyPlan extends Controller_Handler_Api
+{
+   private function normalizeDisciplinesData($data) {
+        if ( !isset($data->id) && isset($data->externalID) ) {
+            $ID = Model_Discipline::withExternalID($data->externalID);
+            if ( $ID != -1 )
+                $data->id = $ID;
+        }
+
+        foreach ($data->teachersHashSnils as $teacherHashSnils) {
+            $ID = Model_Teacher::withINILA($teacherHashSnils);
+            if ( $ID != -1 )
+                $data->teacherIDs[] = $ID;
+        }
+
+        /*
+         * Если нет ни одного преподавателя, то ругаемся в логи и пропускаем дисциплину до лучших времен
+         */
+        if ( !isset($data->teacherIDs) || count($data->teacherIDs) == 0 ) {
+            Log::instance()->add(Log::WARNING, "A discipline without teachers is arrived: {0}",
+                array(
+                    '{0}'   => print_r($data, TRUE)
+                )
+            );
+            return null;
+        }
+
+        switch ($data->type) {
+            case 'Экзамен':
+                $data->type = Model_Discipline::EXAM;
+                break;
+            case 'Зачет':
+                $data->type = Model_Discipline::CREDIT;
+                break;
+            default:
+                Log::instance()->add(Log::WARNING, "A discipline has incorrect type: {0}",
+                    array(
+                        '{0}'   => print_r($data, TRUE)
+                    )
+                );
+                return null;
+        }
+
+        return $data;
+    }
+
+    private function normalizeStudyPlanData($data) {
+        if ( !isset($data->id) && isset($data->externalID) ) {
+            $ID = Model_Plan::withExternalID($data->externalID);
+            if ( $ID != -1 )
+                $data->id = $ID;
+        }
+
+        foreach ($data->disciplines as &$discipline) {
+            $discipline = $this->normalizeDisciplinesData($discipline);
+        }
+
+        return $data;
+    }
+
+    private function createStudyPlan($data) {
+        try {
+
+        } catch (InvalidArgumentException $e) {
+            $this->badRequestError( $this->makeErrorMsg($data, $e->getMessage()) );
+        }
+        return [];
+    }
+
+    private function updateStudyPlan($data) {
+       $res = [];
+        try {
+            foreach ($data->disciplines as $discipline) {
+                if ( $discipline == null )
+                    continue;
+
+                if ( isset($discipline->id) ) {
+                    // ToDo: update discipline
+                } else {
+                    print_r($discipline);
+
+                    $plan = Model_Plan::load($data->id);
+                    $facultyID = $plan->FacultyID;
+                    $semesterID = $plan->SemesterID;
+                    $gradeID = $plan->GradeID;
+
+
+                    $newSubjectID = Model_Subject::create($discipline->name, '', $facultyID);
+
+                    $newDiscipline = Model_Discipline::make();
+                    $newDiscipline->author($discipline->teacherIDs[0]);
+                    $newDiscipline->subject($newSubjectID);
+                    $newDiscipline->type($discipline->type);
+                    $newDiscipline->semester($semesterID);
+                    $newDiscipline->faculty($facultyID);
+                    $newDiscipline->grade($gradeID);
+                    $newDiscipline = $newDiscipline->create();
+                    $res[] = $newDiscipline->ID;
+                }
+            }
+        } catch (InvalidArgumentException $e) {
+            $this->badRequestError( $this->makeErrorMsg($data, $e->getMessage()) );
+        }
+        return $res;
+    }
+
+    private function processData($data) {
+        $data = $this->normalizeStudyPlanData($data);
+        if ($data->id != -1) {
+            return $this->updateStudyPlan($data);
+        } else {
+            return $this->createStudyPlan($data);
+        }
+    }
+
+    /**
+     * @api {put} api/v0/studyPlan Add new study plan(s)
+     * @apiName Add new study plan
+     * @apiGroup Study plans
+     * @apiVersion 0.1.2
+     * @apiParam {String} token Api key
+     * @apiParam {String} [batch] Flag about massive addition
+     * @apiParam {String} externalId
+     * @apiParam {Array} disciplines
+     */
+    public function action_put_index() {
+        try {
+            if ( $this->request->query('batch') !== NULL ) {
+                $data = json_decode($this->request->body());
+
+                $res = [];
+                foreach ($data as $item) {
+                    $res[] = $this->processData($item);
+                }
+            } else {
+                $data = $this->request->query();
+                $res[] = $this->processData($data);
+            }
+        } catch (Exception $e) {
+            $this->badRequestError($e->getMessage());
+        }
+
+        return $res;
+    }
+}
diff --git a/~dev_rating/application/classes/Controller/Api/V0/Teacher.php b/~dev_rating/application/classes/Controller/Api/V0/Teacher.php
index f22ec664f..7ae33e251 100644
--- a/~dev_rating/application/classes/Controller/Api/V0/Teacher.php
+++ b/~dev_rating/application/classes/Controller/Api/V0/Teacher.php
@@ -2,32 +2,6 @@
 
 class Controller_Api_V0_Teacher extends Controller_Handler_Api
 {
-    private function makeErrorMsg($data, $msg) {
-        $dt = print_r($data, true);
-        $res = 'Невозможно загрузить/обновить преподавателя. '.$msg.":\n ".$dt;
-        return $res;
-    }
-
-    private function getDepIDByName($name) {
-        try {
-            $id = Model_Faculties::getDepartmentIdByName($name);
-        }
-        catch (InvalidArgumentException $e) {
-            return NULL;
-        }
-        return $id;
-    }
-
-    private function getDepIDByExternalID($departmentExternalID) {
-        try {
-            $id = Model_Faculties::getDepartmentIdByExternalID($departmentExternalID);
-        }
-        catch (InvalidArgumentException $e) {
-            return NULL;
-        }
-        return $id;
-    }
-
     private function normalizeTeacherData($data) {
         if ( !isset($data->id) && isset($data->externalID) ) {
             $ID = Model_Teacher::withExternalID($data->externalID);
@@ -107,7 +81,7 @@ class Controller_Api_V0_Teacher extends Controller_Handler_Api
             }
             $teacher = $teacher->create();
         } catch (InvalidArgumentException $e) {
-            $this->badRequestError( $this->makeErrorMsg($data, $e->getMessage()) );
+            $this->badRequestError( $this->makeErrorMsg($data, 'Невозможно загрузить/обновить преподавателя. '.$e->getMessage()) );
         }
         return ['ID' => $teacher->ID
                 , 'lastName' => $data->lastName
@@ -122,7 +96,7 @@ class Controller_Api_V0_Teacher extends Controller_Handler_Api
                 $data->jobPositionID, $data->departmentID, $data->status
             );
         } catch (InvalidArgumentException $e) {
-            $this->badRequestError( $this->makeErrorMsg($data, $e->getMessage()) );
+            $this->badRequestError( $this->makeErrorMsg($data, 'Невозможно загрузить/обновить преподавателя. '.$e->getMessage()) );
         }
     }
 
diff --git a/~dev_rating/application/classes/Controller/Handler/Api.php b/~dev_rating/application/classes/Controller/Handler/Api.php
index 2db2726ba..a8765636e 100644
--- a/~dev_rating/application/classes/Controller/Handler/Api.php
+++ b/~dev_rating/application/classes/Controller/Handler/Api.php
@@ -158,4 +158,43 @@ abstract class Controller_Handler_Api extends Controller
             [':uri' => $this->request->uri()]
         )->request($this->request);
     }
+
+    protected function makeErrorMsg($data, $msg) {
+        $dt = print_r($data, true);
+        $res = $msg.":\n ".$dt;
+        return $res;
+    }
+
+    /**
+     * Aux section. It should be extracted somewhere
+     */
+    protected function getDepIDByName($name) {
+        try {
+            $id = Model_Faculties::getDepartmentIdByName($name);
+        }
+        catch (InvalidArgumentException $e) {
+            return NULL;
+        }
+        return $id;
+    }
+
+    protected function getDepIDByExternalID($departmentExternalID) {
+        try {
+            $id = Model_Faculties::getDepartmentIdByExternalID($departmentExternalID);
+        }
+        catch (InvalidArgumentException $e) {
+            return NULL;
+        }
+        return $id;
+    }
+
+    protected function getFacultyIDByExternalID($facultyExternalID) {
+        try {
+            $id = Model_Faculties::getIdByExternalID($facultyExternalID);
+        }
+        catch (InvalidArgumentException $e) {
+            return NULL;
+        }
+        return $id;
+    }
 }
\ No newline at end of file
diff --git a/~dev_rating/application/classes/Model/Discipline.php b/~dev_rating/application/classes/Model/Discipline.php
index bd3e68a19..610c53a23 100644
--- a/~dev_rating/application/classes/Model/Discipline.php
+++ b/~dev_rating/application/classes/Model/Discipline.php
@@ -92,6 +92,14 @@ class Model_Discipline extends Model_Container
         DB::query(Database::SELECT, $sql)->param(':id', $this->ID)->execute();
     }
 
+    public static function withExternalID($extid) {
+        $sql = 'SELECT `Discipline_GetIDFromExternalID`(:ExtID) AS `ID`';
+        $res = DB::query(Database::SELECT, $sql)
+            ->param(':ExtID', $extid)
+            ->execute()->get('ID');
+        return $res;
+    }
+
     // todo: should return Model_Group[]
     public function getGroups() {
         $sql = 'CALL `GetGroupsForDiscipline`(:id)';
diff --git a/~dev_rating/application/classes/Model/Faculties.php b/~dev_rating/application/classes/Model/Faculties.php
index 93a9dc736..0ae0d298c 100644
--- a/~dev_rating/application/classes/Model/Faculties.php
+++ b/~dev_rating/application/classes/Model/Faculties.php
@@ -72,6 +72,18 @@ class Model_Faculties extends Model
         return $id->get('ID');
     }
 
+    public static function getIdByExternalID($facultyExternalID) {
+        $sql = 'CALL `Faculty_GetIdByExternalID`(:depExternalID)';
+        $id = DB::query(Database::SELECT, $sql)
+            ->param(':depExternalID', $facultyExternalID)
+            ->execute();
+
+        if ($id->count() == 0)
+            throw new InvalidArgumentException(Error::DEPARTMENT_NOT_FOUND);
+
+        return $id->get('ID');
+    }
+
     public static function createDepartment($departmentName, $facultyID) {
         $sql = 'SELECT `Department_Create`(:name, :facultyID) AS `ID`;';
         $id = DB::query(Database::SELECT, $sql)
diff --git a/~dev_rating/application/classes/Model/Helper/DisciplineBuilder.php b/~dev_rating/application/classes/Model/Helper/DisciplineBuilder.php
index c5e2f51b6..8b1f79065 100644
--- a/~dev_rating/application/classes/Model/Helper/DisciplineBuilder.php
+++ b/~dev_rating/application/classes/Model/Helper/DisciplineBuilder.php
@@ -7,6 +7,9 @@ class Model_Helper_DisciplineBuilder extends Model_Helper_Builder
             'Subtype' => null,
             'IsLocked' => false,
             'Milestone' => 0,
+            'Lectures' => 0,
+            'Practice' => 0,
+            'Labs' => 0
         ];
 
         $required = [
diff --git a/~dev_rating/application/classes/Model/Helper/PlanBuilder.php b/~dev_rating/application/classes/Model/Helper/PlanBuilder.php
new file mode 100644
index 000000000..3c98e3cd3
--- /dev/null
+++ b/~dev_rating/application/classes/Model/Helper/PlanBuilder.php
@@ -0,0 +1,17 @@
+<?php
+
+class Model_Helper_PlanBuilder extends Model_Helper_Builder
+{
+    public function create() {
+        $this->data += [
+
+        ];
+
+        $required = [];
+
+        if (array_diff($required, array_keys($this->data)))
+            throw new InvalidArgumentException('Not enough arguments');
+
+        return new Model_Plan($this->data, false);
+    }
+}
diff --git a/~dev_rating/application/classes/Model/Helper/RecordBookBuilder.php b/~dev_rating/application/classes/Model/Helper/RecordBookBuilder.php
new file mode 100644
index 000000000..e7ee3a38e
--- /dev/null
+++ b/~dev_rating/application/classes/Model/Helper/RecordBookBuilder.php
@@ -0,0 +1,44 @@
+<?php
+
+class Model_Helper_RecordBookBuilder extends Model_Helper_Builder
+{
+    public function create() {
+        $required = [
+            'StudentID', 'PlanID', 'FacultyID',
+        ];
+
+        if (array_diff($required, array_keys($this->data)))
+            throw new InvalidArgumentException('Not enough arguments');
+
+        return new Model_RecordBook($this->data, false);
+    }
+
+
+    public function & studentID($id) {
+        if (!is_numeric($id) || $id <= 0)
+            throw new InvalidArgumentException('student id is incorrect');
+        $this->data['StudentID'] = (int) $id;
+        return $this;
+    }
+
+    public function & planID($id) {
+        if (!is_numeric($id) || $id <= 0)
+            throw new InvalidArgumentException('plan id is incorrect');
+        $this->data['PlanID'] = (int) $id;
+        return $this;
+    }
+
+    public function & facultyID($id) {
+        if (!is_numeric($id) || $id <= 0)
+            throw new InvalidArgumentException('Faculty id is incorrect');
+        $this->data['FacultyID'] = (int) $id;
+        return $this;
+    }
+
+    public function & externalID($id) {
+        $id = trim($id);
+
+        $this->data['ExternalID'] = (int) $id;
+        return $this;
+    }
+}
diff --git a/~dev_rating/application/classes/Model/Plan.php b/~dev_rating/application/classes/Model/Plan.php
new file mode 100644
index 000000000..47f63fd29
--- /dev/null
+++ b/~dev_rating/application/classes/Model/Plan.php
@@ -0,0 +1,61 @@
+<?php defined('SYSPATH') or die('No direct script access.');
+
+/**
+ * Class Model_Plan
+ *
+ * @property-read   $ID             int
+ * @property        $ExternalID     string
+ * @property        $FacultyID      int
+ * @property        $SemesterID     int
+ * @property        $GradeID        int
+ */
+class Model_Plan extends Model_Container
+{
+    protected function getRawData($id) {
+        $sql = 'CALL `Plan_GetInfo`(:id)';
+        $info = DB::query(Database::SELECT, $sql)
+            ->param(':id', $id)->execute();
+
+        if ($info->count() == 0)
+            throw new InvalidArgumentException('Study plan not found');
+
+        return $info->offsetGet(0);
+    }
+
+    public static function make() {
+        return new Model_Helper_PlanBuilder();
+    }
+
+    protected function create() {
+//        $sql = 'SELECT `CreateStudentGroupSearch`(LastName, FirstName, SecondName, GradeID, GroupNum, FacultyID, ActivationCode, SemesterID) AS `ID`';
+//
+//        $this->data[self::$ID_FIELD] = DB::query(Database::SELECT, $sql)
+//            ->parameters($this->data)
+//            ->execute()->get('ID');
+//
+//        if ($this->ID <= 0)
+//            throw new InvalidArgumentException(Error::INVALID_PARAMETERS);
+    }
+
+    public static function withExternalID($extid) {
+        $sql = 'SELECT `Plan_GetIDFromExternalID`(:ExtID) AS `ID`';
+        $res = DB::query(Database::SELECT, $sql)
+            ->param(':ExtID', $extid)
+            ->execute()->get('ID');
+        return $res;
+    }
+
+    /** @return Model_Discipline[] */
+    public function getDisciplines($semesterID = null) {
+        throw new BadMethodCallException('Method is not implemented yet!');
+    }
+
+    public function getTeachers($loadAll = false, $semesterID = null) {
+        throw new BadMethodCallException('Method is not implemented yet!');
+    }
+
+    // todo implementation
+    public function update() {
+        throw new BadMethodCallException('Method is not implemented yet!');
+    }
+}
diff --git a/~dev_rating/application/classes/Model/RecordBook.php b/~dev_rating/application/classes/Model/RecordBook.php
new file mode 100644
index 000000000..9c6bc8a2d
--- /dev/null
+++ b/~dev_rating/application/classes/Model/RecordBook.php
@@ -0,0 +1,135 @@
+<?php defined('SYSPATH') or die('No direct script access.');
+
+/**
+ * Class Model_RecordBook
+ *
+ * @property-read   $ID             int
+ * @property        $ExternalID     string
+ * @property        $PlanID         int
+ * @property        $SubdivisionID  int
+ * @property        $Status         string
+ * @property        $GroupID        int
+ * @property        $GroupNum       int
+ * @property        $GroupName      string
+ * @property        $GradeID        int
+ * @property        $GradeNum       int
+ * @property        $Degree         string
+ * @property        $SpecID         int
+ * @property        $SpecName       string
+ * @property        $SpecAbbr       string
+ * @property        $SpecCode       string
+ * @property        $FacultyID      int
+ * @property        $FacultyName    string
+ * @property        $FacultyAbbr    string
+ */
+class Model_RecordBook extends Model_Container
+{
+    protected function getRawData($id) {
+//        $sql = 'CALL `Student_GetInfo`(:id)';
+//        $info = DB::query(Database::SELECT, $sql)
+//            ->param(':id', $id)->execute();
+//
+//        if ($info->count() == 0)
+//            throw new InvalidArgumentException('Student not found');
+//
+//        $sql = 'CALL `Student_GetRecordBooks`(:id)';
+//        $res = DB::query(Database::SELECT, $sql)
+//            ->param(':id', $id)->execute()->as_array();
+//
+//        $recordBooks = [];
+//        foreach ($res as $recordBook)
+//            $recordBooks[$recordBook['ID']] = $recordBook;
+//
+//        return array_merge($info[0], array('RecordBooks' => $recordBooks));
+        throw new HTTP_Exception_500("Not implemented");
+    }
+
+    public static function make() {
+        return new Model_Helper_RecordBookBuilder();
+    }
+
+    protected function create() {
+        $sql = 'SELECT `RecordBook_Create`(ExternalID, StudentID, PlanID, FacultyID) AS `ID`';
+
+        $this->data[self::$ID_FIELD] = DB::query(Database::SELECT, $sql)
+            ->parameters($this->data)
+            ->execute()->get('ID');
+
+        if ($this->ID <= 0)
+            throw new InvalidArgumentException(Error::INVALID_PARAMETERS);
+    }
+
+    public static function withExternalID($extid) {
+        $sql = 'SELECT `RecordBook_GetIDFromExternalID`(:ExtID) AS `ID`';
+        $res = DB::query(Database::SELECT, $sql)
+            ->param(':ExtID', $extid)
+            ->execute()->get('ID');
+        return $res;
+    }
+
+    /** @return Model_Discipline[] */
+    public function getDisciplines($semesterID = null) {
+        $semesterID = $semesterID ?: User::instance()->SemesterID;
+        $recordBookID = $this->ID;
+        
+        $sql = 'CALL `Student_GetDisciplines`(:recordBookID, :semesterID)';
+        $query = DB::query(Database::SELECT, $sql)
+            ->param(':recordBookID', $recordBookID)
+            ->param(':semesterID', $semesterID)
+            ->execute();
+
+        $list = [];
+        foreach ($query as $data)
+            $list[] = new Model_Discipline($data, true);
+
+        return $list;
+    }
+
+    public function getTeachers($loadAll = false, $semesterID = null) {
+        // todo: don't load the full data at the Controller_Student_Index
+        $semesterID = $semesterID ?: User::instance()->SemesterID;
+        $recordBookID = $this->ID;
+
+        $sql = 'CALL `Student_GetTeachersList`(:recordBookID, :semesterID, :loadAll)';
+        $result = DB::query(Database::SELECT, $sql)
+            ->param(':recordBookID', $recordBookID)
+            ->param(':semesterID', $semesterID)
+            ->param(':loadAll', $loadAll)
+            ->execute();
+
+        $list = [];
+        foreach ($result as $row) {
+            $id = $row['DisciplineID'];
+            $list[$id][] = $row;
+        }
+        return $list;
+    }
+
+    // todo implementation
+    public function update() {
+        throw new BadMethodCallException('Method is not implemented yet!');
+    }
+
+    /**
+     * Set the student's state and group in a semester.
+     * @param $group int  group id
+     * @return $this;
+     */
+    public function setState($group, $state="common", $semesterID = null) {
+        $semesterID = $semesterID ?: User::instance()->SemesterID;;
+        $sql = 'SELECT `ControlStudentGroup`(:id, :group, :state, :semesterID) AS Result';
+
+        DB::query(Database::SELECT, $sql)
+            ->param(':id', $this->ID)
+            ->param(':group', (int) $group)
+            ->param(':state', $state)
+            ->param(':semesterID', $semesterID)
+            ->execute();
+
+        return $this;
+    }
+
+    public function transferIntoGroup($group, $semesterID = null) {
+        return $this->setState($group, "common", $semesterID);
+    }
+}
diff --git a/~dev_rating/application/classes/Model/Student.php b/~dev_rating/application/classes/Model/Student.php
index 0ca7262ae..ab046fae8 100644
--- a/~dev_rating/application/classes/Model/Student.php
+++ b/~dev_rating/application/classes/Model/Student.php
@@ -84,7 +84,15 @@ class Model_Student extends Model_Container
 
         return $response == -1 ? -1 : $code;
     }
-    
+
+    public static function withExternalID($extid) {
+        $sql = 'SELECT `Student_GetIDFromExternalID`(:ExtID) AS `ID`';
+        $res = DB::query(Database::SELECT, $sql)
+            ->param(':ExtID', $extid)
+            ->execute()->get('ID');
+        return $res;
+    }
+
     /** @return Model_Discipline[] */
     public function getDisciplines($semesterID = null, $recordBookID = null) {
         $semesterID = $semesterID ?: User::instance()->SemesterID;
diff --git a/~dev_rating/application/classes/Model/Subject.php b/~dev_rating/application/classes/Model/Subject.php
index 3bb8f6354..832c931a4 100644
--- a/~dev_rating/application/classes/Model/Subject.php
+++ b/~dev_rating/application/classes/Model/Subject.php
@@ -35,7 +35,6 @@ class Model_Subject
         return self::MARK_A;
     }
 
-
     public static function create($name, $abbr, $facultyID) {        
         $sql = 'SELECT `CreateSubject`(:faculty, :name, :abbr) AS `Num`';
         $res = DB::query(Database::SELECT, $sql)
@@ -47,4 +46,12 @@ class Model_Subject
 
         return (int) $res->get('Num');
     }
+
+    public static function withExternalID($extid) {
+        $sql = 'SELECT `Plan_GetIDFromExternalID`(:ExtID) AS `ID`';
+        $res = DB::query(Database::SELECT, $sql)
+            ->param(':ExtID', $extid)
+            ->execute()->get('ID');
+        return $res;
+    }
 }
diff --git a/~dev_rating/application/classes/Model/Teacher.php b/~dev_rating/application/classes/Model/Teacher.php
index 1161f186c..7efcec091 100644
--- a/~dev_rating/application/classes/Model/Teacher.php
+++ b/~dev_rating/application/classes/Model/Teacher.php
@@ -56,6 +56,14 @@ class Model_Teacher extends Model_Container
         return $res;
     }
 
+    public static function withINILA($extid) {
+        $sql = 'SELECT `Teacher_GetIDFromINILA`(:ExtID) AS `ID`';
+        $res = DB::query(Database::SELECT, $sql)
+            ->param(':ExtID', $extid)
+            ->execute()->get('ID');
+        return $res;
+    }
+
     protected function create() {
         if ( isset($this->data['ExternalID']) ) {
             $sql = 'SELECT `Teacher_CreateActivated`(LastName, FirstName, SecondName, JobPositionID, DepID, ExternalID, INILA) AS `ID`';
diff --git a/~dev_rating/application/routes/api/v0.php b/~dev_rating/application/routes/api/v0.php
index c118e9012..6eddb79d8 100644
--- a/~dev_rating/application/routes/api/v0.php
+++ b/~dev_rating/application/routes/api/v0.php
@@ -128,4 +128,17 @@ Route::set('apiv0:subject:another', 'api/v0/subject/<action>(/<id>)', ['id' => '
         'action'    => 'index',
         'directory' => 'Api/V0',
         'controller' => 'Subject',
+    ]);
+
+Route::set('apiv0:studyPlan', 'api/v0/studyPlan')
+    ->filter(function($route, $params, $request)
+    {
+        // Prefix the method to the action name
+        $params['action'] = strtolower($request->method()).'_'.$params['action'];
+        return $params; // Returning an array will replace the parameters
+    })
+    ->defaults([
+        'action'    => 'index',
+        'directory' => 'Api/V0',
+        'controller' => 'StudyPlan',
     ]);
\ No newline at end of file
diff --git a/~dev_rating/application/views/office/sync.twig b/~dev_rating/application/views/office/sync.twig
index 5f7208eaa..cb8b3c337 100644
--- a/~dev_rating/application/views/office/sync.twig
+++ b/~dev_rating/application/views/office/sync.twig
@@ -20,11 +20,12 @@
 		</div>
 
 		<div>
-			<button class="defaultForm GreenButton" id="syncStudents">Синхронизировать студентов</button>
+			<button class="defaultForm GreenButton" id="syncPlans">Синхронизировать учебные планы</button>
 		</div>
 
 		<div>
-			<button class="defaultForm GreenButton" id="syncPlans">Синхронизировать учебные планы</button>
+			<button class="defaultForm GreenButton" id="syncStudents">Синхронизировать студентов</button>
+
 		</div>
 
 		<div>
-- 
GitLab