Files
codecanyon-rest-api-for-per…/api/models/Proposals_model.php
2025-10-29 11:09:43 +01:00

921 lines
47 KiB
PHP

<?php
use app\services\AbstractKanban;
use app\services\proposals\ProposalsPipeline;
defined('BASEPATH') or exit('No direct script access allowed');
class Proposals_model extends App_Model {
private $statuses;
private $copy = false;
public function __construct() {
parent::__construct();
$this->statuses = hooks()->apply_filters('before_set_proposal_statuses', [6, 4, 1, 5, 2, 3, ]);
}
public function get_statuses() {
return $this->statuses;
}
public function get_sale_agents($playground = false) {
return $this->db->query('SELECT DISTINCT(assigned) as sale_agent FROM ' . db_prefix() . ($playground ? 'playground_' : '') . 'proposals WHERE assigned != 0')->result_array();
}
public function get_proposals_years($playground = false) {
return $this->db->query('SELECT DISTINCT(YEAR(date)) as year FROM ' . db_prefix() . ($playground ? 'playground_' : '') . 'proposals')->result_array();
}
/**
* Inserting new proposal function
* @param mixed $data $_POST data
*/
public function add($data, $playground = false) {
$data['allow_comments'] = isset($data['allow_comments']) ? 1 : 0;
$save_and_send = isset($data['save_and_send']);
$tags = isset($data['tags']) ? $data['tags'] : '';
if (isset($data['custom_fields'])) {
$custom_fields = $data['custom_fields'];
unset($data['custom_fields']);
}
$estimateRequestID = false;
if (isset($data['estimate_request_id'])) {
$estimateRequestID = $data['estimate_request_id'];
unset($data['estimate_request_id']);
}
$data['address'] = trim($data['address']);
$data['address'] = nl2br($data['address']);
$data['datecreated'] = date('Y-m-d H:i:s');
$data['addedfrom'] = get_staff_user_id();
$data['hash'] = app_generate_hash();
if (empty($data['rel_type'])) {
unset($data['rel_type']);
unset($data['rel_id']);
} else {
if (empty($data['rel_id'])) {
unset($data['rel_type']);
unset($data['rel_id']);
}
}
$items = [];
if (isset($data['newitems'])) {
$items = $data['newitems'];
unset($data['newitems']);
}
if ($this->copy == false) {
$data['content'] = '{proposal_items}';
}
if (isset($data['rel_id'], $data['rel_type']) && $data['rel_type'] !== 'customer') {
$data['project_id'] = null;
}
$hook = hooks()->apply_filters('before_create_proposal', ['data' => $data, 'items' => $items, ]);
$data = $hook['data'];
$items = $hook['items'];
$this->db->insert(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', $data);
$insert_id = $this->db->insert_id();
if ($insert_id) {
if ($estimateRequestID !== false && $estimateRequestID != '') {
$this->load->model('estimate_request_model');
$completedStatus = $this->estimate_request_model->get_status_by_flag('completed', $playground);
$this->estimate_request_model->update_request_status(['requestid' => $estimateRequestID, 'status' => $completedStatus->id, ], $playground);
}
if (isset($custom_fields)) {
$this->load->model('custom_fields_model');
$this->custom_fields_model->handle_custom_fields_post($insert_id, $custom_fields, false, $playground);
}
$this->load->model('misc_model');
$this->misc_model->handle_tags_save($tags, $insert_id, 'proposal', $playground);
$this->load->model('invoice_items_model');
foreach ($items as $key => $item) {
if ($itemid = $this->invoice_items_model->add_new_sales_item_post($item, $insert_id, 'proposal', $playground)) {
$this->invoice_items_model->_maybe_insert_post_item_tax($itemid, $item, $insert_id, 'proposal', $playground);
}
}
$proposal = $this->get($insert_id, [], false, $playground);
if ($proposal->assigned != 0) {
if ($proposal->assigned != get_staff_user_id()) {
$notified = add_notification(['description' => 'not_proposal_assigned_to_you', 'touserid' => $proposal->assigned, 'fromuserid' => get_staff_user_id(), 'link' => 'proposals/list_proposals/' . $insert_id, 'additional_data' => serialize([$proposal->subject, ]), ]);
if ($notified) {
pusher_trigger_notification([$proposal->assigned]);
}
}
}
if ($data['rel_type'] == 'lead') {
$this->load->model('leads_model');
$this->leads_model->log_lead_activity($data['rel_id'], 'not_lead_activity_created_proposal', false, serialize(['<a href="' . admin_url('proposals/list_proposals/' . $insert_id) . '" target="_blank">' . $data['subject'] . '</a>', ]), $playground);
}
update_sales_total_tax_column($insert_id, 'proposal', db_prefix() . ($playground ? 'playground_' : '') . 'proposals');
log_activity('New Proposal Created [ID: ' . $insert_id . ']');
if ($save_and_send === true) {
$this->send_proposal_to_email($insert_id, true, '', $playground);
}
hooks()->do_action('proposal_created', $insert_id);
return $insert_id;
}
return false;
}
/**
* Update proposal
* @param mixed $data $_POST data
* @param mixed $id proposal id
* @return boolean
*/
public function update($data, $id, $playground = false) {
$affectedRows = 0;
$data['allow_comments'] = isset($data['allow_comments']) ? 1 : 0;
$current_proposal = $this->get($id, [], false, $playground);
$save_and_send = isset($data['save_and_send']);
if (empty($data['rel_type'])) {
$data['rel_id'] = null;
$data['rel_type'] = '';
} else {
if (empty($data['rel_id'])) {
$data['rel_id'] = null;
$data['rel_type'] = '';
}
}
if (isset($data['custom_fields'])) {
$custom_fields = $data['custom_fields'];
$this->load->model('custom_fields_model');
if ($this->custom_fields_model->handle_custom_fields_post($id, $custom_fields, false, $playground)) {
$affectedRows++;
}
unset($data['custom_fields']);
}
$items = [];
if (isset($data['items'])) {
$items = $data['items'];
unset($data['items']);
}
$newitems = [];
if (isset($data['newitems'])) {
$newitems = $data['newitems'];
unset($data['newitems']);
}
if (isset($data['tags'])) {
$this->load->model('misc_model');
if ($this->misc_model->handle_tags_save($data['tags'], $id, 'proposal', $playground)) {
$affectedRows++;
}
}
$data['address'] = trim($data['address']);
$data['address'] = nl2br($data['address']);
$hook = hooks()->apply_filters('before_proposal_updated', ['data' => $data, 'items' => $items, 'newitems' => $newitems, 'removed_items' => isset($data['removed_items']) ? $data['removed_items'] : [], ], $id);
$data = $hook['data'];
$data['removed_items'] = $hook['removed_items'];
$newitems = $hook['newitems'];
$items = $hook['items'];
// Delete items checked to be removed from database
foreach ($data['removed_items'] as $remove_item_id) {
if (handle_removed_sales_item_post($remove_item_id, 'proposal')) {
$affectedRows++;
}
}
unset($data['removed_items']);
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', $data);
if ($this->db->affected_rows() > 0) {
$affectedRows++;
$proposal_now = $this->get($id, [], false, $playground);
if ($current_proposal->assigned != $proposal_now->assigned) {
if ($proposal_now->assigned != get_staff_user_id()) {
$notified = add_notification(['description' => 'not_proposal_assigned_to_you', 'touserid' => $proposal_now->assigned, 'fromuserid' => get_staff_user_id(), 'link' => 'proposals/list_proposals/' . $id, 'additional_data' => serialize([$proposal_now->subject, ]), ]);
if ($notified) {
pusher_trigger_notification([$proposal_now->assigned]);
}
}
}
}
$this->load->model('custom_fields_model');
foreach ($items as $key => $item) {
if (update_sales_item_post($item['itemid'], $item)) {
$affectedRows++;
}
if (isset($item['custom_fields'])) {
if ($this->custom_fields_model->handle_custom_fields_post($item['itemid'], $item['custom_fields'], false, $playground)) {
$affectedRows++;
}
}
if (!isset($item['taxname']) || (isset($item['taxname']) && count($item['taxname']) == 0)) {
if (delete_taxes_from_item($item['itemid'], 'proposal')) {
$affectedRows++;
}
} else {
$item_taxes = $this->get_proposal_item_taxes($item['itemid'], $playground);
$_item_taxes_names = [];
foreach ($item_taxes as $_item_tax) {
array_push($_item_taxes_names, $_item_tax['taxname']);
}
$i = 0;
foreach ($_item_taxes_names as $_item_tax) {
if (!in_array($_item_tax, $item['taxname'])) {
$this->db->where('id', $item_taxes[$i]['id'])->delete(db_prefix() . ($playground ? 'playground_' : '') . 'item_tax');
if ($this->db->affected_rows() > 0) {
$affectedRows++;
}
}
$i++;
}
if (_maybe_insert_post_item_tax($item['itemid'], $item, $id, 'proposal')) {
$affectedRows++;
}
}
}
$this->load->model('invoice_items_model');
foreach ($newitems as $key => $item) {
if ($new_item_added = $this->invoice_items_model->add_new_sales_item_post($item, $id, 'proposal', $playground)) {
$this->invoice_items_model->_maybe_insert_post_item_tax($new_item_added, $item, $id, 'proposal', $playground);
$affectedRows++;
}
}
if ($affectedRows > 0) {
update_sales_total_tax_column($id, 'proposal', db_prefix() . ($playground ? 'playground_' : '') . 'proposals');
log_activity('Proposal Updated [ID:' . $id . ']');
}
if ($save_and_send === true) {
$this->send_proposal_to_email($id, true, '', $playground);
}
if ($affectedRows > 0) {
hooks()->do_action('after_proposal_updated', $id);
return true;
}
return false;
}
/**
* Get proposals
* @param mixed $id proposal id OPTIONAL
* @return mixed
*/
public function get($id = '', $where = [], $for_editor = false, $playground = false) {
$this->db->where($where);
if (is_client_logged_in()) {
$this->db->where('status !=', 0);
}
$this->db->select('*,' . db_prefix() . ($playground ? 'playground_' : '') . 'currencies.id as currencyid, ' . db_prefix() . ($playground ? 'playground_' : '') . 'proposals.id as id, ' . db_prefix() . ($playground ? 'playground_' : '') . 'currencies.name as currency_name');
$this->db->from(db_prefix() . ($playground ? 'playground_' : '') . 'proposals');
$this->db->join(db_prefix() . ($playground ? 'playground_' : '') . 'currencies', db_prefix() . ($playground ? 'playground_' : '') . 'currencies.id = ' . db_prefix() . ($playground ? 'playground_' : '') . 'proposals.currency', 'left');
if (is_numeric($id)) {
$this->db->where(db_prefix() . ($playground ? 'playground_' : '') . 'proposals.id', $id);
$proposal = $this->db->get()->row();
if ($proposal) {
$proposal->attachments = $this->get_attachments($id, $playground);
$this->load->model('invoice_items_model');
$proposal->items = $this->invoice_items_model->get_items_by_type('proposal', $id, $playground);
$proposal->visible_attachments_to_customer_found = false;
foreach ($proposal->attachments as $attachment) {
if ($attachment['visible_to_customer'] == 1) {
$proposal->visible_attachments_to_customer_found = true;
break;
}
}
if ($proposal->project_id) {
$this->load->model('projects_model');
$proposal->project_data = $this->projects_model->get($proposal->project_id, $playground);
}
if ($for_editor == false) {
$proposal = parse_proposal_content_merge_fields($proposal, $playground);
}
}
return $proposal;
}
return $this->db->get()->result_array();
}
public function clear_signature($id, $playground = false) {
$this->db->select('signature');
$this->db->where('id', $id);
$proposal = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'proposals')->row();
$this->load->model('misc_model');
if ($proposal) {
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['signature' => null]);
if (!empty($proposal->signature)) {
unlink($this->misc_model->get_upload_path_by_type('proposal', $playground) . $id . '/' . $proposal->signature);
}
return true;
}
return false;
}
public function update_pipeline($data, $playground = false) {
$this->mark_action_status($data['status'], $data['proposalid'], false, $playground);
AbstractKanban::updateOrder($data['order'], 'pipeline_order', ($playground ? 'playground_' : '') . 'proposals', $data['status']);
}
public function get_attachments($proposal_id, $id = '', $playground = false) {
// If is passed id get return only 1 attachment
if (is_numeric($id)) {
$this->db->where('id', $id);
} else {
$this->db->where('rel_id', $proposal_id);
}
$this->db->where('rel_type', 'proposal');
$result = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'files');
if (is_numeric($id)) {
return $result->row();
}
return $result->result_array();
}
/**
* Delete proposal attachment
* @param mixed $id attachmentid
* @return boolean
*/
public function delete_attachment($id, $playground = false) {
$attachment = $this->get_attachments('', $id, $playground);
$deleted = false;
if ($attachment) {
if (empty($attachment->external)) {
unlink(get_upload_path_by_type('proposal', $playground) . $attachment->rel_id . '/' . $attachment->file_name);
}
$this->db->where('id', $attachment->id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'files');
if ($this->db->affected_rows() > 0) {
$deleted = true;
log_activity('Proposal Attachment Deleted [ID: ' . $attachment->rel_id . ']');
}
if (is_dir(get_upload_path_by_type('proposal') . $attachment->rel_id)) {
// Check if no attachments left, so we can delete the folder also
$other_attachments = list_files(get_upload_path_by_type('proposal', $playground) . $attachment->rel_id);
if (count($other_attachments) == 0) {
// okey only index.html so we can delete the folder also
delete_dir(get_upload_path_by_type('proposal', $playground) . $attachment->rel_id);
}
}
}
return $deleted;
}
/**
* Add proposal comment
* @param mixed $data $_POST comment data
* @param boolean $client is request coming from the client side
*/
public function add_comment($data, $client = false, $playground = false) {
if (is_staff_logged_in()) {
$client = false;
}
if (isset($data['action'])) {
unset($data['action']);
}
$data['dateadded'] = date('Y-m-d H:i:s');
if ($client == false) {
$data['staffid'] = get_staff_user_id();
}
$data['content'] = nl2br($data['content']);
$this->db->insert(db_prefix() . ($playground ? 'playground_' : '') . 'proposal_comments', $data);
$insert_id = $this->db->insert_id();
if ($insert_id) {
$proposal = $this->get($data['proposalid'], [], false, $playground);
// No notifications client when proposal is with draft status
if ($proposal->status == '6' && $client == false) {
return true;
}
if ($client == true) {
// Get creator and assigned
$this->db->select('staffid,email,phonenumber');
$this->db->where('staffid', $proposal->addedfrom);
$this->db->or_where('staffid', $proposal->assigned);
$staff_proposal = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'staff')->result_array();
$notifiedUsers = [];
foreach ($staff_proposal as $member) {
$notified = add_notification(['description' => 'not_proposal_comment_from_client', 'touserid' => $member['staffid'], 'fromcompany' => 1, 'fromuserid' => 0, 'link' => 'proposals/list_proposals/' . $data['proposalid'], 'additional_data' => serialize([$proposal->subject, ]), ], $playground);
if ($notified) {
array_push($notifiedUsers, $member['staffid']);
}
$template = mail_template('proposal_comment_to_staff', $proposal->id, $member['email']);
$merge_fields = $template->get_merge_fields();
$template->send();
// Send email/sms to admin that client commented
$this->app_sms->trigger(SMS_TRIGGER_PROPOSAL_NEW_COMMENT_TO_STAFF, $member['phonenumber'], $merge_fields);
}
hooks()->do_action('after_proposal_client_add_comment', $proposal->id);
pusher_trigger_notification($notifiedUsers);
} else {
// Send email/sms to client that admin commented
$template = mail_template('proposal_comment_to_customer', $proposal);
$merge_fields = $template->get_merge_fields();
$template->send();
$this->app_sms->trigger(SMS_TRIGGER_PROPOSAL_NEW_COMMENT_TO_CUSTOMER, $proposal->phone, $merge_fields);
hooks()->do_action('after_proposal_staff_add_comment', $proposal->id);
}
return true;
}
return false;
}
public function edit_comment($data, $id, $playground = false) {
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposal_comments', ['content' => nl2br($data['content']), ]);
if ($this->db->affected_rows() > 0) {
return true;
}
return false;
}
/**
* Get proposal comments
* @param mixed $id proposal id
* @return array
*/
public function get_comments($id, $playground = false) {
$this->db->where('proposalid', $id);
$this->db->order_by('dateadded', 'ASC');
return $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'proposal_comments')->result_array();
}
/**
* Get proposal single comment
* @param mixed $id comment id
* @return object
*/
public function get_comment($id, $playground = false) {
$this->db->where('id', $id);
return $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'proposal_comments')->row();
}
/**
* Remove proposal comment
* @param mixed $id comment id
* @return boolean
*/
public function remove_comment($id, $playground = false) {
$comment = $this->get_comment($id, $playground);
$this->db->where('id', $id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'proposal_comments');
if ($this->db->affected_rows() > 0) {
log_activity('Proposal Comment Removed [ProposalID:' . $comment->proposalid . ', Comment Content: ' . $comment->content . ']');
return true;
}
return false;
}
/**
* Copy proposal
* @param mixed $id proposal id
* @return mixed
*/
public function copy($id, $playground = false) {
$this->copy = true;
$proposal = $this->get($id, [], true, $playground);
$not_copy_fields = ['addedfrom', 'id', 'datecreated', 'hash', 'status', 'invoice_id', 'estimate_id', 'is_expiry_notified', 'date_converted', 'signature', 'acceptance_firstname', 'acceptance_lastname', 'acceptance_email', 'acceptance_date', 'acceptance_ip', ];
$fields = $this->db->list_fields(db_prefix() . ($playground ? 'playground_' : '') . 'proposals');
$insert_data = [];
foreach ($fields as $field) {
if (!in_array($field, $not_copy_fields)) {
$insert_data[$field] = $proposal->$field;
}
}
$insert_data['addedfrom'] = get_staff_user_id();
$insert_data['datecreated'] = date('Y-m-d H:i:s');
$insert_data['date'] = _d(date('Y-m-d'));
$insert_data['status'] = 6;
$insert_data['hash'] = app_generate_hash();
// in case open till is expired set new 7 days starting from current date
if ($insert_data['open_till'] && get_option('proposal_due_after') != 0) {
$insert_data['open_till'] = _d(date('Y-m-d', strtotime('+' . get_option('proposal_due_after') . ' DAY', strtotime(date('Y-m-d')))));
} else if ($insert_data['open_till']) {
$dDate = new DateTime(date('Y-m-d'));
$dOpenTill = new DateTime($insert_data['open_till']);
$dDiff = $dDate->diff($dOpenTill);
$insert_data['open_till'] = _d($dDate->modify('+ ' . $dDiff->days . ' DAY')->format('Y-m-d'));
}
$insert_data['newitems'] = [];
$this->load->model('custom_fields_model');
$custom_fields_items = $this->custom_fields_model->get_custom_fields('items', [], false, $playground);
$key = 1;
foreach ($proposal->items as $item) {
$insert_data['newitems'][$key]['description'] = $item['description'];
$insert_data['newitems'][$key]['long_description'] = clear_textarea_breaks($item['long_description']);
$insert_data['newitems'][$key]['qty'] = $item['qty'];
$insert_data['newitems'][$key]['unit'] = $item['unit'];
$insert_data['newitems'][$key]['taxname'] = [];
$taxes = $this->get_proposal_item_taxes($item['id'], $playground);
foreach ($taxes as $tax) {
// tax name is in format TAX1|10.00
array_push($insert_data['newitems'][$key]['taxname'], $tax['taxname']);
}
$insert_data['newitems'][$key]['rate'] = $item['rate'];
$insert_data['newitems'][$key]['order'] = $item['item_order'];
foreach ($custom_fields_items as $cf) {
$insert_data['newitems'][$key]['custom_fields']['items'][$cf['id']] = $this->custom_fields_model->get_custom_field_value($item['id'], $cf['id'], 'items', false, $playground);
if (!defined('COPY_CUSTOM_FIELDS_LIKE_HANDLE_POST')) {
define('COPY_CUSTOM_FIELDS_LIKE_HANDLE_POST', true);
}
}
$key++;
}
$id = $this->add($insert_data, $playground);
if ($id) {
$this->load->model('custom_fields_model');
$custom_fields = $this->custom_fields_model->get_custom_fields('proposal', [], false, $playground);
foreach ($custom_fields as $field) {
$value = $this->custom_fields_model->get_custom_field_value($proposal->id, $field['id'], 'proposal', false, $playground);
if ($value == '') {
continue;
}
$this->db->insert(db_prefix() . ($playground ? 'playground_' : '') . 'customfieldsvalues', ['relid' => $id, 'fieldid' => $field['id'], 'fieldto' => 'proposal', 'value' => $value, ]);
}
$tags = get_tags_in($proposal->id, 'proposal', $playground);
$this->load->model('misc_model');
$this->misc_model->handle_tags_save($tags, $id, 'proposal', $playground);
log_activity('Copied Proposal ' . format_proposal_number($proposal->id));
return $id;
}
return false;
}
/**
* Take proposal action (change status) manually
* @param mixed $status status id
* @param mixed $id proposal id
* @param boolean $client is request coming from client side or not
* @return boolean
*/
public function mark_action_status($status, $id, $client = false, $playground = false) {
$original_proposal = $this->get($id, false, [], $playground);
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['status' => $status, ]);
if ($this->db->affected_rows() > 0) {
// Client take action
if ($client == true) {
$revert = false;
// Declined
if ($status == 2) {
$message = 'not_proposal_proposal_declined';
} else if ($status == 3) {
// Accepted
if (get_option('proposal_auto_convert_to_invoice_on_client_accept') == '1') {
$this->convert_to_invoice($id, $playground);
}
$message = 'not_proposal_proposal_accepted';
} else {
$revert = true;
}
// This is protection that only 3 and 4 statuses can be taken as action from the client side
if ($revert == true) {
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['status' => $original_proposal->status, ]);
return false;
}
// Get creator and assigned;
$this->db->where('staffid', $original_proposal->addedfrom);
$this->db->or_where('staffid', $original_proposal->assigned);
$staff_proposal = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'staff')->result_array();
$notifiedUsers = [];
foreach ($staff_proposal as $member) {
$notified = add_notification(['fromcompany' => true, 'touserid' => $member['staffid'], 'description' => $message, 'link' => 'proposals/list_proposals/' . $id, 'additional_data' => serialize([format_proposal_number($id), ]), ]);
if ($notified) {
array_push($notifiedUsers, $member['staffid']);
}
}
pusher_trigger_notification($notifiedUsers);
// Send thank you to the customer email template
if ($status == 3) {
foreach ($staff_proposal as $member) {
send_mail_template('proposal_accepted_to_staff', $original_proposal, $member['email']);
}
send_mail_template('proposal_accepted_to_customer', $original_proposal);
hooks()->do_action('proposal_accepted', $id);
} else {
// Client declined send template to admin
foreach ($staff_proposal as $member) {
send_mail_template('proposal_declined_to_staff', $original_proposal, $member['email']);
}
hooks()->do_action('proposal_declined', $id);
}
} else {
// in case admin mark as open the the open till date is smaller then current date set open till date 7 days more
if ((date('Y-m-d', strtotime($original_proposal->open_till)) < date('Y-m-d')) && $status == 1) {
$open_till = date('Y-m-d', strtotime('+7 DAY', strtotime(date('Y-m-d'))));
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['open_till' => $open_till, ]);
}
}
log_activity('Proposal Status Changes [ProposalID:' . $id . ', Status:' . format_proposal_status($status, '', false) . ',Client Action: ' . (int)$client . ']');
return true;
}
return false;
}
/**
* Delete proposal
* @param mixed $id proposal id
* @return boolean
*/
public function delete($id, $playground = false) {
hooks()->do_action('before_proposal_deleted', $id);
$this->clear_signature($id, $playground);
$proposal = $this->get($id, [], false, $playground);
$this->db->where('id', $id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'proposals');
if ($this->db->affected_rows() > 0) {
if (!is_null($proposal->short_link)) {
app_archive_short_link($proposal->short_link, $playground);
}
$this->load->model('misc_model');
$this->misc_model->delete_tracked_emails($id, 'proposal', $playground);
$this->db->where('proposalid', $id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'proposal_comments');
// Get related tasks
$this->db->where('rel_type', 'proposal');
$this->db->where('rel_id', $id);
$tasks = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'tasks')->result_array();
$this->load->model('tasks_model');
foreach ($tasks as $task) {
$this->tasks_model->delete_task($task['id'], true, $playground);
}
$attachments = $this->get_attachments($id, $playground);
foreach ($attachments as $attachment) {
$this->delete_attachment($attachment['id'], $playground);
}
$this->db->where('rel_id', $id);
$this->db->where('rel_type', 'proposal');
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'notes');
$this->db->where('relid IN (SELECT id from ' . db_prefix() . ($playground ? 'playground_' : '') . 'itemable WHERE rel_type="proposal" AND rel_id="' . $this->db->escape_str($id) . '")');
$this->db->where('fieldto', 'items');
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'customfieldsvalues');
$this->db->where('rel_id', $id);
$this->db->where('rel_type', 'proposal');
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'itemable');
$this->db->where('rel_id', $id);
$this->db->where('rel_type', 'proposal');
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'item_tax');
$this->db->where('rel_id', $id);
$this->db->where('rel_type', 'proposal');
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'taggables');
// Delete the custom field values
$this->db->where('relid', $id);
$this->db->where('fieldto', 'proposal');
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'customfieldsvalues');
$this->db->where('rel_type', 'proposal');
$this->db->where('rel_id', $id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'reminders');
$this->db->where('rel_type', 'proposal');
$this->db->where('rel_id', $id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'views_tracking');
log_activity('Proposal Deleted [ProposalID:' . $id . ']');
hooks()->do_action('after_proposal_deleted', $id);
return true;
}
return false;
}
/**
* Get relation proposal data. Ex lead or customer will return the necesary db fields
* @param mixed $rel_id
* @param string $rel_type customer/lead
* @return object
*/
public function get_relation_data_values($rel_id, $rel_type, $playground = false) {
$data = new StdClass();
if ($rel_type == 'customer') {
$this->load->model('clients_model');
$this->db->where('userid', $rel_id);
$_data = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'clients')->row();
$primary_contact_id = $this->clients_model->get_primary_contact_user_id($rel_id, $playground);
if ($primary_contact_id) {
$contact = $this->clients_model->get_contact($primary_contact_id, ['active' => 1], [], $playground);
$data->email = $contact->email;
}
$data->phone = $_data->phonenumber;
$data->is_using_company = false;
if (isset($contact)) {
$data->to = $contact->firstname . ' ' . $contact->lastname;
} else {
if (!empty($_data->company)) {
$data->to = $_data->company;
$data->is_using_company = true;
}
}
$data->company = $_data->company;
$data->address = clear_textarea_breaks($_data->address, $playground);
$data->zip = $_data->zip;
$data->country = $_data->country;
$data->state = $_data->state;
$data->city = $_data->city;
$default_currency = $this->clients_model->get_customer_default_currency($rel_id);
if ($default_currency != 0) {
$data->currency = $default_currency;
}
} else if ($rel_type = 'lead') {
$this->db->where('id', $rel_id);
$_data = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'leads')->row();
$data->phone = $_data->phonenumber;
$data->is_using_company = false;
if (empty($_data->company)) {
$data->to = $_data->name;
} else {
$data->to = $_data->company;
$data->is_using_company = true;
}
$data->company = $_data->company;
$data->address = $_data->address;
$data->email = $_data->email;
$data->zip = $_data->zip;
$data->country = $_data->country;
$data->state = $_data->state;
$data->city = $_data->city;
}
return $data;
}
/**
* Sent proposal to email
* @param mixed $id proposalid
* @param string $template email template to sent
* @param boolean $attachpdf attach proposal pdf or not
* @return boolean
*/
public function send_expiry_reminder($id, $playground = false) {
$proposal = $this->get($id, [], false, $playground);
// For all cases update this to prevent sending multiple reminders eq on fail
$this->db->where('id', $proposal->id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['is_expiry_notified' => 1, ]);
$template = mail_template('proposal_expiration_reminder', $proposal);
$merge_fields = $template->get_merge_fields();
$template->send();
if (can_send_sms_based_on_creation_date($proposal->datecreated, $playground)) {
$sms_sent = $this->app_sms->trigger(SMS_TRIGGER_PROPOSAL_EXP_REMINDER, $proposal->phone, $merge_fields);
}
hooks()->do_action('after_proposal_expiry_reminder_sent', $id);
return true;
}
public function send_proposal_to_email($id, $attachpdf = true, $cc = '', $playground = false) {
// Proposal status is draft update to sent
if (total_rows(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['id' => $id, 'status' => 6]) > 0) {
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['status' => 4]);
}
$proposal = $this->get($id, [], false, $playground);
$sent = send_mail_template('proposal_send_to_customer', $proposal, $attachpdf, $cc, $playground);
if ($sent) {
// Set to status sent
$this->db->where('id', $id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['status' => 4, ]);
hooks()->do_action('proposal_sent', $id);
return true;
}
return false;
}
public function do_kanban_query($status, $search = '', $page = 1, $sort = [], $count = false, $playground = false) {
_deprecated_function('Proposal_model::do_kanban_query', '2.9.2', 'ProposalsPipeline class');
$kanBan = (new ProposalsPipeline($status))->search($search)->page($page)->sortBy($sort['sort']??null, $sort['sort_by']??null);
if ($count) {
return $kanBan->countAll();
}
return $kanBan->get();
}
/**
* Convert proposal to invoice
* @param mixed $id proposal id
* @return mixed New invoice ID
*/
public function convert_to_invoice($id, $playground = false) {
// Recurring invoice date is okey lets convert it to new invoice
$proposal = $this->get($id, [], false, $playground);
if ($proposal->rel_type != 'customer') {
return false;
}
$new_invoice_data = [];
$new_invoice_data['clientid'] = $proposal->rel_id;
$new_invoice_data['project_id'] = $proposal->project_id;
$new_invoice_data['number'] = get_option('next_invoice_number');
$new_invoice_data['date'] = _d(date('Y-m-d'));
$new_invoice_data['duedate'] = _d(date('Y-m-d'));
if (get_option('invoice_due_after') != 0) {
$new_invoice_data['duedate'] = _d(date('Y-m-d', strtotime('+' . get_option('invoice_due_after') . ' DAY', strtotime(date('Y-m-d')))));
}
$new_invoice_data['show_quantity_as'] = $proposal->show_quantity_as;
$new_invoice_data['currency'] = $proposal->currency;
$new_invoice_data['subtotal'] = $proposal->subtotal;
$new_invoice_data['total'] = $proposal->total;
$new_invoice_data['adjustment'] = $proposal->adjustment;
$new_invoice_data['discount_percent'] = $proposal->discount_percent;
$new_invoice_data['discount_total'] = $proposal->discount_total;
$new_invoice_data['discount_type'] = $proposal->discount_type;
$new_invoice_data['sale_agent'] = $proposal->assigned;
$new_invoice_data['billing_street'] = clear_textarea_breaks($proposal->address);
$new_invoice_data['billing_city'] = $proposal->city;
$new_invoice_data['billing_state'] = $proposal->state;
$new_invoice_data['billing_zip'] = $proposal->zip;
$new_invoice_data['billing_country'] = $proposal->country;
$new_invoice_data['shipping_street'] = '';
$new_invoice_data['shipping_city'] = '';
$new_invoice_data['shipping_state'] = '';
$new_invoice_data['shipping_zip'] = '';
$new_invoice_data['shipping_country'] = '';
$new_invoice_data['include_shipping'] = 0;
$new_invoice_data['show_shipping_on_invoice'] = 0;
$new_invoice_data['terms'] = get_option('predefined_terms_invoice');
$new_invoice_data['clientnote'] = get_option('predefined_clientnote_invoice');
// Set to unpaid status automatically
$new_invoice_data['status'] = 1;
$new_invoice_data['adminnote'] = '';
$this->load->model('payment_modes_model');
$modes = $this->payment_modes_model->get('', ['expenses_only !=' => 1, ]);
$temp_modes = [];
foreach ($modes as $mode) {
if ($mode['selected_by_default'] == 0) {
continue;
}
$temp_modes[] = $mode['id'];
}
$new_invoice_data['allowed_payment_modes'] = $temp_modes;
$new_invoice_data['newitems'] = [];
$key = 1;
foreach ($proposal->items as $item) {
$new_invoice_data['newitems'][$key]['description'] = $item['description'];
$new_invoice_data['newitems'][$key]['long_description'] = clear_textarea_breaks($item['long_description']);
$new_invoice_data['newitems'][$key]['qty'] = $item['qty'];
$new_invoice_data['newitems'][$key]['unit'] = $item['unit'];
$new_invoice_data['newitems'][$key]['taxname'] = [];
$taxes = $this->get_proposal_item_taxes($item['id'], $playground);
foreach ($taxes as $tax) {
// tax name is in format TAX1|10.00
array_push($new_invoice_data['newitems'][$key]['taxname'], $tax['taxname']);
}
$new_invoice_data['newitems'][$key]['rate'] = $item['rate'];
$new_invoice_data['newitems'][$key]['order'] = $item['item_order'];
$key++;
}
$this->load->model('invoices_model');
$invoice_id = $this->invoices_model->add($new_invoice_data, $playground);
if ($invoice_id) {
// Customer accepted the estimate and is auto converted to invoice
if (!is_staff_logged_in()) {
$this->db->where('rel_type', 'invoice');
$this->db->where('rel_id', $invoice_id);
$this->db->delete(db_prefix() . ($playground ? 'playground_' : '') . 'sales_activity');
$this->invoices_model->log_invoice_activity($id, 'invoice_activity_auto_converted_from_proposal', true, serialize(['<a href="' . admin_url('proposals#' . $proposal->id) . '">' . format_proposal_number($proposal->id) . '</a>', ]), $playground);
}
// For all cases update addefrom and sale agent from the invoice
// May happen staff is not logged in and these values to be 0
$this->db->where('id', $invoice_id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'invoices', ['addedfrom' => $proposal->addedfrom, 'sale_agent' => $proposal->assigned, ]);
// Update estimate with the new invoice data and set to status accepted
$this->db->where('id', $proposal->id);
$this->db->update(db_prefix() . ($playground ? 'playground_' : '') . 'proposals', ['invoice_id' => $invoice_id, 'status' => 3, ]);
if (is_custom_fields_smart_transfer_enabled()) {
$this->db->where('fieldto', 'proposal');
$this->db->where('active', 1);
$cfProposals = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'customfields')->result_array();
foreach ($cfProposals as $field) {
$tmpSlug = explode('_', $field['slug'], 2);
if (isset($tmpSlug[1])) {
$this->db->where('fieldto', 'invoice');
$this->db->group_start();
$this->db->like('slug', 'invoice_' . $tmpSlug[1], 'after');
$this->db->where('type', $field['type']);
$this->db->where('options', $field['options']);
$this->db->where('active', 1);
$this->db->group_end();
// $this->db->where('slug LIKE "invoice_' . $tmpSlug[1] . '%" AND type="' . $field['type'] . '" AND options="' . $field['options'] . '" AND active=1');
$cfTransfer = $this->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'customfields')->result_array();
// Don't make mistakes
// Only valid if 1 result returned
// + if field names similarity is equal or more then CUSTOM_FIELD_TRANSFER_SIMILARITY%
if (count($cfTransfer) == 1 && ((similarity($field['name'], $cfTransfer[0]['name']) * 100) >= CUSTOM_FIELD_TRANSFER_SIMILARITY)) {
$value = get_custom_field_value($proposal->id, $field['id'], 'estimate', false, $playground);
if ($value == '') {
continue;
}
$this->db->insert(db_prefix() . ($playground ? 'playground_' : '') . 'customfieldsvalues', ['relid' => $id, 'fieldid' => $cfTransfer[0]['id'], 'fieldto' => 'invoice', 'value' => $value, ]);
}
}
}
}
hooks()->do_action('after_proposal_converted_to_invoice', ['proposal_id' => $id, 'invoice_id' => $invoice_id]);
log_activity('Proposal Converted to Invoice [InvoiceID: ' . $invoice_id . ', ProposalID: ' . $id . ']');
}
return $id;
}
/**
* Function that return proposal item taxes based on passed item id
* @param mixed $itemid
* @return array
*/
public function get_proposal_item_taxes($itemid, $playground = false)
{
$CI = &get_instance();
$CI->db->where('itemid', $itemid);
$CI->db->where('rel_type', 'proposal');
$taxes = $CI->db->get(db_prefix() . ($playground ? 'playground_' : '') . 'item_tax')->result_array();
$i = 0;
foreach ($taxes as $tax) {
$taxes[$i]['taxname'] = $tax['taxname'] . '|' . $tax['taxrate'];
$i++;
}
return $taxes;
}
}