@ -0,0 +1,41 @@ |
|||
Export Product Stock in Excel v15 |
|||
================================= |
|||
This module helps you to take current stock report for all products in each warehouse. |
|||
|
|||
Configuration |
|||
============= |
|||
* No additional configurations needed |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
Credits |
|||
------- |
|||
* Developers: Cybrosys Techno Solutions odoo@cybrosys.com |
|||
Version 15: Midilaj V K @cybrosys |
|||
|
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. image:: https://cybrosys.com/images/logo.png |
|||
:target: https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
|
|||
For support and more information, please visit `Our Website <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
|||
|
|||
|
@ -0,0 +1,24 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
|
|||
from . import models |
|||
from . import controllers |
@ -0,0 +1,54 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
|
|||
{ |
|||
'name': 'Export Product Stock in Excel', |
|||
'version': '15.0.1.0.0', |
|||
'live_test_url': 'https://www.youtube.com/watch?v=9ae4GkApHQM', |
|||
'summary': "Current Stock Report for all Products in each Warehouse", |
|||
'description': "Current Stock Report for all Products in each Warehouse, Odoo 13,Odoo13", |
|||
'category': 'Warehouse', |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'website': 'https://www.cybrosys.com', |
|||
'depends': [ |
|||
'base', |
|||
'stock', |
|||
'sale', |
|||
'purchase', |
|||
], |
|||
'data': [ |
|||
'views/wizard_view.xml', |
|||
'security/ir.model.access.csv', |
|||
], |
|||
'images': ['static/description/banner.png'], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'export_stockinfo_xls/static/src/js/action_manager.js', |
|||
], |
|||
}, |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'auto_install': False, |
|||
} |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import main |
@ -0,0 +1,57 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
|
|||
import json |
|||
from odoo import http |
|||
from odoo.http import content_disposition, request |
|||
from odoo.addons.web.controllers.main import _serialize_exception |
|||
from odoo.tools import html_escape |
|||
|
|||
|
|||
class XLSXReportController(http.Controller): |
|||
|
|||
@http.route('/xlsx_reports', type='http', auth='user', methods=['POST'], csrf=False) |
|||
def get_report_xlsx(self, model, options, output_format, report_name, **kw): |
|||
uid = request.session.uid |
|||
report_obj = request.env[model].with_user(uid) |
|||
options = json.loads(options) |
|||
token = 'dummy-because-api-expects-one' |
|||
try: |
|||
if output_format == 'xlsx': |
|||
response = request.make_response( |
|||
None, |
|||
headers=[ |
|||
('Content-Type', 'application/vnd.ms-excel'), |
|||
('Content-Disposition', content_disposition(report_name + '.xlsx')) |
|||
] |
|||
) |
|||
report_obj.get_xlsx_report(options, response) |
|||
response.set_cookie('fileToken', token) |
|||
return response |
|||
except Exception as e: |
|||
se = _serialize_exception(e) |
|||
error = { |
|||
'code': 200, |
|||
'message': 'Odoo Server Error', |
|||
'data': se |
|||
} |
|||
return request.make_response(html_escape(json.dumps(error))) |
@ -0,0 +1,11 @@ |
|||
## Module <export_stockinfo_xls> |
|||
|
|||
#### 04.10.2021 |
|||
#### Version 15.0.1.0.0 |
|||
#### ADD |
|||
Initial Commit Export Product Stock in Excel |
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,24 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
|
|||
from . import res_partner |
|||
from . import wizard |
@ -0,0 +1,41 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
|
|||
from odoo import models, fields |
|||
|
|||
|
|||
class Partner(models.Model): |
|||
_inherit = 'res.partner' |
|||
|
|||
supplier_id = fields.Many2many('wizard.stock.history', 'supp_wiz_rel', 'wiz', 'supp', invisible=True) |
|||
|
|||
|
|||
class Category(models.Model): |
|||
_inherit = 'product.category' |
|||
|
|||
obj = fields.Many2many('wizard.stock.history', 'categ_wiz_rel', 'wiz', 'categ', invisible=True) |
|||
|
|||
|
|||
class Warehouse(models.Model): |
|||
_inherit = 'stock.warehouse' |
|||
|
|||
obj = fields.Many2many('wizard.stock.history', 'wh_wiz_rel', 'wiz', 'wh', invisible=True) |
@ -0,0 +1,249 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Midilaj (<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import time |
|||
from datetime import date, datetime |
|||
import pytz |
|||
import json |
|||
import datetime |
|||
import io |
|||
from odoo import api, fields, models, _ |
|||
from odoo.tools import date_utils |
|||
try: |
|||
from odoo.tools.misc import xlsxwriter |
|||
except ImportError: |
|||
import xlsxwriter |
|||
|
|||
|
|||
class StockReport(models.TransientModel): |
|||
_name = "wizard.stock.history" |
|||
_description = "Current Stock History" |
|||
|
|||
warehouse = fields.Many2many('stock.warehouse', 'wh_wiz_rel', 'wh', 'wiz', string='Warehouse', required=True) |
|||
category = fields.Many2many('product.category', 'categ_wiz_rel', 'categ', 'wiz', string='Warehouse') |
|||
|
|||
def export_xls(self): |
|||
data = { |
|||
'ids': self.ids, |
|||
'model': self._name, |
|||
'warehouse': self.warehouse.ids, |
|||
'category': self.category.ids, |
|||
|
|||
} |
|||
return { |
|||
'type': 'ir.actions.report', |
|||
'data': {'model': 'wizard.stock.history', |
|||
'options': json.dumps(data, default=date_utils.json_default), |
|||
'output_format': 'xlsx', |
|||
'report_name': 'Current Stock History', |
|||
}, |
|||
'report_type': 'stock_xlsx' |
|||
} |
|||
|
|||
def get_warehouse(self, data): |
|||
wh = data.warehouse.mapped('id') |
|||
obj = self.env['stock.warehouse'].search([('id', 'in', wh)]) |
|||
l1 = [] |
|||
l2 = [] |
|||
for j in obj: |
|||
l1.append(j.name) |
|||
l2.append(j.id) |
|||
return l1, l2 |
|||
|
|||
def get_lines(self, data, warehouse): |
|||
lines = [] |
|||
categ_id = data.mapped('id') |
|||
if categ_id: |
|||
categ_products = self.env['product.product'].search([('categ_id', 'in', categ_id)]) |
|||
|
|||
else: |
|||
categ_products = self.env['product.product'].search([]) |
|||
product_ids = tuple([pro_id.id for pro_id in categ_products]) |
|||
sale_query = """ |
|||
SELECT sum(s_o_l.product_uom_qty) AS product_uom_qty, s_o_l.product_id FROM sale_order_line AS s_o_l |
|||
JOIN sale_order AS s_o ON s_o_l.order_id = s_o.id |
|||
WHERE s_o.state IN ('sale','done') |
|||
AND s_o.warehouse_id = %s |
|||
AND s_o_l.product_id in %s group by s_o_l.product_id""" |
|||
purchase_query = """ |
|||
SELECT sum(p_o_l.product_qty) AS product_qty, p_o_l.product_id FROM purchase_order_line AS p_o_l |
|||
JOIN purchase_order AS p_o ON p_o_l.order_id = p_o.id |
|||
INNER JOIN stock_picking_type AS s_p_t ON p_o.picking_type_id = s_p_t.id |
|||
WHERE p_o.state IN ('purchase','done') |
|||
AND s_p_t.warehouse_id = %s AND p_o_l.product_id in %s group by p_o_l.product_id""" |
|||
params = warehouse, product_ids if product_ids else (0, 0) |
|||
self._cr.execute(sale_query, params) |
|||
sol_query_obj = self._cr.dictfetchall() |
|||
self._cr.execute(purchase_query, params) |
|||
pol_query_obj = self._cr.dictfetchall() |
|||
for obj in categ_products: |
|||
sale_value = 0 |
|||
purchase_value = 0 |
|||
for sol_product in sol_query_obj: |
|||
if sol_product['product_id'] == obj.id: |
|||
sale_value = sol_product['product_uom_qty'] |
|||
for pol_product in pol_query_obj: |
|||
if pol_product['product_id'] == obj.id: |
|||
purchase_value = pol_product['product_qty'] |
|||
virtual_available = obj.with_context({'warehouse': warehouse}).virtual_available |
|||
outgoing_qty = obj.with_context({'warehouse': warehouse}).outgoing_qty |
|||
incoming_qty = obj.with_context({'warehouse': warehouse}).incoming_qty |
|||
available_qty = virtual_available + outgoing_qty - incoming_qty |
|||
value = available_qty * obj.standard_price |
|||
vals = { |
|||
'sku': obj.default_code, |
|||
'name': obj.name, |
|||
'category': obj.categ_id.name, |
|||
'cost_price': obj.standard_price, |
|||
'available': available_qty, |
|||
'virtual': virtual_available, |
|||
'incoming': incoming_qty, |
|||
'outgoing': outgoing_qty, |
|||
'net_on_hand': obj.with_context({'warehouse': warehouse}).qty_available, |
|||
'total_value': value, |
|||
'sale_value': sale_value, |
|||
'purchase_value': purchase_value, |
|||
} |
|||
lines.append(vals) |
|||
return lines |
|||
|
|||
def get_xlsx_report(self, data, response): |
|||
output = io.BytesIO() |
|||
workbook = xlsxwriter.Workbook(output, {'in_memory': True}) |
|||
lines = self.browse(data['ids']) |
|||
d = lines.category |
|||
get_warehouse = self.get_warehouse(lines) |
|||
count = len(get_warehouse[0]) * 11 + 6 |
|||
comp = self.env.user.company_id.name |
|||
sheet = workbook.add_worksheet('Stock Info') |
|||
format0 = workbook.add_format({'font_size': 20, 'align': 'center', 'bold': True}) |
|||
format1 = workbook.add_format({'font_size': 14, 'align': 'vcenter', 'bold': True}) |
|||
format11 = workbook.add_format({'font_size': 12, 'align': 'center', 'bold': True}) |
|||
format21 = workbook.add_format({'font_size': 10, 'align': 'center', 'bold': True}) |
|||
format3 = workbook.add_format({'bottom': True, 'top': True, 'font_size': 12}) |
|||
format4 = workbook.add_format({'font_size': 12, 'align': 'left', 'bold': True}) |
|||
font_size_8 = workbook.add_format({'font_size': 8, 'align': 'center'}) |
|||
font_size_8_l = workbook.add_format({'font_size': 8, 'align': 'left'}) |
|||
font_size_8_r = workbook.add_format({'font_size': 8, 'align': 'right'}) |
|||
red_mark = workbook.add_format({'font_size': 8, 'bg_color': 'red'}) |
|||
justify = workbook.add_format({'font_size': 12}) |
|||
format3.set_align('center') |
|||
justify.set_align('justify') |
|||
format1.set_align('center') |
|||
red_mark.set_align('center') |
|||
sheet.merge_range(1, 7, 2, 10, 'Product Stock Info', format0) |
|||
sheet.merge_range(3, 7, 3, 10, comp, format11) |
|||
w_house = ', ' |
|||
cat = ', ' |
|||
c = [] |
|||
d1 = d.mapped('id') |
|||
if d1: |
|||
for i in d1: |
|||
c.append(self.env['product.category'].browse(i).name) |
|||
cat = cat.join(c) |
|||
sheet.merge_range(4, 0, 4, 1, 'Category(s) : ', format4) |
|||
sheet.merge_range(4, 2, 4, 3 + len(d1), cat, format4) |
|||
sheet.merge_range(5, 0, 5, 1, 'Warehouse(s) : ', format4) |
|||
w_house = w_house.join(get_warehouse[0]) |
|||
sheet.merge_range(5, 2, 5, 3 + len(get_warehouse[0]), w_house, format4) |
|||
user = self.env['res.users'].browse(self.env.uid) |
|||
tz = pytz.timezone(user.tz if user.tz else 'UTC') |
|||
times = pytz.utc.localize(datetime.datetime.now()).astimezone(tz) |
|||
sheet.merge_range('A8:G8', 'Report Date: ' + str(times.strftime("%Y-%m-%d %H:%M %p")), format1) |
|||
sheet.merge_range(7, 7, 7, count, 'Warehouses', format1) |
|||
sheet.merge_range('A9:G9', 'Product Information', format11) |
|||
w_col_no = 6 |
|||
w_col_no1 = 7 |
|||
for i in get_warehouse[0]: |
|||
w_col_no = w_col_no + 11 |
|||
sheet.merge_range(8, w_col_no1, 8, w_col_no, i, format11) |
|||
w_col_no1 = w_col_no1 + 11 |
|||
sheet.write(9, 0, 'SKU', format21) |
|||
sheet.merge_range(9, 1, 9, 3, 'Name', format21) |
|||
sheet.merge_range(9, 4, 9, 5, 'Category', format21) |
|||
sheet.write(9, 6, 'Cost Price', format21) |
|||
p_col_no1 = 7 |
|||
for i in get_warehouse[0]: |
|||
sheet.write(9, p_col_no1, 'Available', format21) |
|||
sheet.write(9, p_col_no1 + 1, 'Virtual', format21) |
|||
sheet.write(9, p_col_no1 + 2, 'Incoming', format21) |
|||
sheet.write(9, p_col_no1 + 3, 'Outgoing', format21) |
|||
sheet.merge_range(9, p_col_no1 + 4, 9, p_col_no1 + 5, 'Net On Hand', format21) |
|||
sheet.merge_range(9, p_col_no1 + 6, 9, p_col_no1 + 7, 'Total Sold', format21) |
|||
sheet.merge_range(9, p_col_no1 + 8, 9, p_col_no1 + 9, 'Total Purchased', format21) |
|||
sheet.write(9, p_col_no1 + 10, 'Valuation', format21) |
|||
p_col_no1 = p_col_no1 + 11 |
|||
prod_row = 10 |
|||
prod_col = 0 |
|||
for i in get_warehouse[1]: |
|||
get_line = self.get_lines(d, i) |
|||
for each in get_line: |
|||
sheet.write(prod_row, prod_col, each['sku'], font_size_8) |
|||
sheet.merge_range(prod_row, prod_col + 1, prod_row, prod_col + 3, each['name'], font_size_8_l) |
|||
sheet.merge_range(prod_row, prod_col + 4, prod_row, prod_col + 5, each['category'], font_size_8_l) |
|||
sheet.write(prod_row, prod_col + 6, each['cost_price'], font_size_8_r) |
|||
prod_row = prod_row + 1 |
|||
break |
|||
prod_row = 10 |
|||
prod_col = 7 |
|||
for i in get_warehouse[1]: |
|||
get_line = self.get_lines(d, i) |
|||
for each in get_line: |
|||
if each['available'] < 0: |
|||
sheet.write(prod_row, prod_col, each['available'], red_mark) |
|||
else: |
|||
sheet.write(prod_row, prod_col, each['available'], font_size_8) |
|||
if each['virtual'] < 0: |
|||
sheet.write(prod_row, prod_col + 1, each['virtual'], red_mark) |
|||
else: |
|||
sheet.write(prod_row, prod_col + 1, each['virtual'], font_size_8) |
|||
if each['incoming'] < 0: |
|||
sheet.write(prod_row, prod_col + 2, each['incoming'], red_mark) |
|||
else: |
|||
sheet.write(prod_row, prod_col + 2, each['incoming'], font_size_8) |
|||
if each['outgoing'] < 0: |
|||
sheet.write(prod_row, prod_col + 3, each['outgoing'], red_mark) |
|||
else: |
|||
sheet.write(prod_row, prod_col + 3, each['outgoing'], font_size_8) |
|||
if each['net_on_hand'] < 0: |
|||
sheet.merge_range(prod_row, prod_col + 4, prod_row, prod_col + 5, each['net_on_hand'], red_mark) |
|||
else: |
|||
sheet.merge_range(prod_row, prod_col + 4, prod_row, prod_col + 5, each['net_on_hand'], font_size_8) |
|||
if each['sale_value'] < 0: |
|||
sheet.merge_range(prod_row, prod_col + 6, prod_row, prod_col + 7, each['sale_value'], red_mark) |
|||
else: |
|||
sheet.merge_range(prod_row, prod_col + 6, prod_row, prod_col + 7, each['sale_value'], font_size_8) |
|||
if each['purchase_value'] < 0: |
|||
sheet.merge_range(prod_row, prod_col + 8, prod_row, prod_col + 9, each['purchase_value'], red_mark) |
|||
else: |
|||
sheet.merge_range(prod_row, prod_col + 8, prod_row, prod_col + 9, each['purchase_value'], |
|||
font_size_8) |
|||
if each['total_value'] < 0: |
|||
sheet.write(prod_row, prod_col + 10, each['total_value'], red_mark) |
|||
else: |
|||
sheet.write(prod_row, prod_col + 10, each['total_value'], font_size_8_r) |
|||
prod_row = prod_row + 1 |
|||
prod_row = 10 |
|||
prod_col = prod_col + 11 |
|||
workbook.close() |
|||
output.seek(0) |
|||
response.stream.write(output.read()) |
|||
output.close() |
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 266 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 173 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,602 @@ |
|||
<div class="container" style="padding: 1rem !important; margin-bottom: 1rem !important;"> |
|||
<div class="row"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12 d-flex justify-content-between" |
|||
style="border-bottom: 1px solid #d5d5d5;"> |
|||
<div class="my-3"> |
|||
<img src="./assets/icons/logo.png" style="width: auto !important; height: 40px !important;"> |
|||
</div> |
|||
<div class="my-3 d-flex align-items-center"> |
|||
<div |
|||
style="background-color: #7C7BAD !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;"> |
|||
<i class="fa fa-check mr-1"></i>Community |
|||
</div> |
|||
<div |
|||
style="background-color: #875A7B !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;"> |
|||
<i class="fa fa-check mr-1"></i>Enterprise |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="container" style="padding: 0rem 1.5rem 4rem !important"> |
|||
<div class="row" style="height: 900px !important;"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12" |
|||
style="padding: 4rem 1rem !important; background-color: #714B67 !important; height: 600px !important; border-radius: 20px !important;"> |
|||
<h1 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #FFFFFF !important; font-size: 3.5rem !important; text-align: center !important;"> |
|||
Current Stock XLS</h1> |
|||
<p |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 300 !important; color: #FFFFFF !important; font-size: 1.4rem !important; text-align: center !important;"> |
|||
Current Stock Report for all Products in each Warehouse |
|||
</p> |
|||
<img src="./assets/screenshots/hero.png" class="img-responsive" width="100%" height="auto" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin-bottom: 2rem !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-compass mr-2"></i>Explore this module |
|||
</h2> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<a href="#overview" style="text-decoration: none !important;"> |
|||
<div class="row" |
|||
style="background-color: #f5f2f5 !important; border-radius: 10px !important; margin: 1rem !important; padding: 1.5em !important; height: 100px !important;"> |
|||
<div class="col-8"> |
|||
<h3 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.2rem !important;"> |
|||
Overview</h3> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #714B67 !important; font-size: 0.9rem !important;"> |
|||
Learn more about this module</p> |
|||
</div> |
|||
<div class="col-4 text-right d-flex justify-content-end align-items-center"> |
|||
<i class="fa fa-chevron-right" style="color: #714B67 !important;"></i> |
|||
</div> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<a href="#features" style="text-decoration: none !important;"> |
|||
<div class="row" |
|||
style="background-color: #f5f2f5 !important; border-radius: 10px !important; margin: 1rem !important; padding: 1.5em !important; height: 100px !important;"> |
|||
<div class="col-8"> |
|||
<h3 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.2rem !important;"> |
|||
Features</h3> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #714B67 !important; font-size: 0.9rem !important;"> |
|||
View features of this module</p> |
|||
</div> |
|||
<div class="col-4 text-right d-flex justify-content-end align-items-center"> |
|||
<i class="fa fa-chevron-right" style="color: #714B67 !important;"></i> |
|||
</div> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<a href="#screenshots" style="text-decoration: none !important;"> |
|||
<div class="row" |
|||
style="background-color: #f5f2f5 !important; border-radius: 10px !important; margin: 1rem !important; padding: 1.5em !important; height: 100px !important;"> |
|||
<div class="col-8"> |
|||
<h3 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.2rem !important;"> |
|||
Screenshots</h3> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #714B67 !important; font-size: 0.9rem !important;"> |
|||
See key screenshots of this module</p> |
|||
</div> |
|||
<div class="col-4 text-right d-flex justify-content-end align-items-center"> |
|||
<i class="fa fa-chevron-right" style="color: #714B67 !important;"></i> |
|||
</div> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<div class="row" id="overview"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-pie-chart mr-2"></i>Overview |
|||
</h2> |
|||
</div> |
|||
|
|||
<div class="col-mg-12 pl-3"> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important; line-height: 30px !important;"> |
|||
This module helps to print Current Stock Report for all Products in each Warehouse with XLS</p> |
|||
|
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<div class="row" id="features"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-star mr-2"></i>Features |
|||
</h2> |
|||
</div> |
|||
|
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<h4 |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Community & Enterprise Support</h4> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important;"> |
|||
Available in Odoo 14.0 Community and Enterprise.</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<h4 |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Current Stock XLS</h4> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important;"> |
|||
Current Stock Report for all Products in each Warehouse.</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<h4 |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Select category for products</h4> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<h4 |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Get your stock valuation details</h4> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<h4 |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Negative stock will be highlighted in "red" cells.</h4> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<div class="row" id="screenshots"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-image mr-2"></i>Screenshots |
|||
</h2> |
|||
</div> |
|||
<div class="col-lg-12 my-2"> |
|||
<h4 class="mt-2" |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Export Stock Info Wizard</h4> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important;"> |
|||
Go to Inventory -> Reports -> Current stock in Excel. Now a wizard will appear on your screen. |
|||
Please enter the warehouses which you want to take the report. |
|||
You can also select category for products(It is Optional). Then Click "Export Product with Stock |
|||
Info" button. Then You will get the corresponding report in XLS.</p> |
|||
<img src="assets/screenshots/stock_xlsx1.png" class="img-responsive img-thumbnail border" width="100%" |
|||
height="auto" /> |
|||
</div> |
|||
|
|||
<div class="col-lg-12 my-3"> |
|||
<h4 class="mt-3" |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 600 !important; color: #282F33 !important; font-size: 1.3rem !important;"> |
|||
Stock Info Excel Report</h4> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important;"> |
|||
Per warehouse you can get Available Qty, Virtual Qty, Incoming Qty, Outgoing Qty, Net On Hand Qty, |
|||
Total Sold & Total Purchased Qty. |
|||
You can get your stock valuation details too. |
|||
Negative stock will be highlighted in "red" cells. |
|||
</p> |
|||
<img src="assets/screenshots/stock_xlsx2.png" class="img-responsive img-thumbnail border" width="100%" |
|||
height="auto" /> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<!-- SUGGESTED PRODUCTS --> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center" |
|||
style="text-align: center; padding: 2.5rem 1rem !important;"> |
|||
<h2 style="color: #212529 !important;">Suggested Products</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
|
|||
<div id="demo1" class="row carousel slide" data-ride="carousel"> |
|||
<!-- The slideshow --> |
|||
<div class="carousel-inner"> |
|||
<div class="carousel-item active" style="min-height:0px"> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/14.0/export_stockinfo_xls/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/export_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/14.0/dashboard_pos/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/pos_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/14.0/product_approval_management/" |
|||
target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/approval_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="carousel-item" style="min-height:0px"> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/14.0/base_account_budget/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/budget_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/14.0/shopify_odoo_connector/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/shopify_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/14.0/odoo11_magento2/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/magento_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- Left and right controls --> |
|||
<a class="carousel-control-prev" href="#demo1" data-slide="prev" |
|||
style="left:-25px;width: 35px;color: #000;"> <span class="carousel-control-prev-icon"><i |
|||
class="fa fa-chevron-left" style="font-size:24px"></i></span> </a> <a |
|||
class="carousel-control-next" href="#demo1" data-slide="next" |
|||
style="right:-25px;width: 35px;color: #000;"> |
|||
<span class="carousel-control-next-icon"><i class="fa fa-chevron-right" |
|||
style="font-size:24px"></i></span> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- END OF SUGGESTED PRODUCTS --> |
|||
|
|||
<!-- OUR SERVICES --> |
|||
<section class="container" style="margin-top: 6rem !important;"> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style="color: #212529 !important;">Our Services</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #1dd1a1 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/cogs.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Customization</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #ff6b6b !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/wrench.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Implementation</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #6462CD !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/lifebuoy.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Support</h6> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #ffa801 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/user.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Hire |
|||
Odoo |
|||
Developer</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #54a0ff !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/puzzle.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Integration</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #6d7680 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/update.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Migration</h6> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #786fa6 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/consultation.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Consultancy</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #f8a5c2 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/training.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Implementation</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #e6be26 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/license.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Licensing Consultancy</h6> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<!-- END OF END OF OUR SERVICES --> |
|||
|
|||
<!-- OUR INDUSTRIES --> |
|||
<section class="container" style="margin-top: 6rem !important;"> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style="color: #212529 !important;">Our Industries</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/trading-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Trading |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Easily procure |
|||
and |
|||
sell your products</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/pos-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
POS |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Easy |
|||
configuration |
|||
and convivial experience</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/education-black.png" class="img-responsive mb-3" height="48px" |
|||
width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Education |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
A platform for |
|||
educational management</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/manufacturing-black.png" class="img-responsive mb-3" height="48px" |
|||
width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Manufacturing |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Plan, track and |
|||
schedule your operations</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/ecom-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
E-commerce & Website |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Mobile |
|||
friendly, |
|||
awe-inspiring product pages</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/service-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Service Management |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Keep track of |
|||
services and invoice</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/restaurant-black.png" class="img-responsive mb-3" height="48px" |
|||
width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Restaurant |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Run your bar or |
|||
restaurant methodically</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/hotel-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 |
|||
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Hotel Management |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
An |
|||
all-inclusive |
|||
hotel management application</p> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</section> |
|||
|
|||
<!-- END OF END OF OUR INDUSTRIES --> |
|||
|
|||
<!-- FOOTER --> |
|||
<!-- Footer Section --> |
|||
<section class="container" style="margin: 5rem auto 2rem;"> |
|||
<div class="row" style="max-width:1540px;"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style="color: #212529 !important;">Need Help?</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Contact Cards --> |
|||
<div class="row d-flex justify-content-center align-items-center" |
|||
style="max-width:1540px; margin: 0 auto 2rem auto;"> |
|||
|
|||
<div class="col-lg-12" style="padding: 0rem 3rem 2rem; border-radius: 10px; margin-right: 3rem; "> |
|||
|
|||
<div class="row mt-4"> |
|||
<div class="col-lg-6"> |
|||
<a href="mailto:odoo@cybrosys.com" target="_blank" class="btn btn-block mb-2 deep_hover" |
|||
style="text-decoration: none; background-color: #4d4d4d; color: #FFF; border-radius: 4px;"><i |
|||
class="fa fa-envelope mr-2"></i>odoo@cybrosys.com</a> |
|||
</div> |
|||
<div class="col-lg-6"> |
|||
<a href="https://api.whatsapp.com/send?phone=918606827707" target="_blank" |
|||
class="btn btn-block mb-2 deep_hover" |
|||
style="text-decoration: none; background-color: #25D366; color: #FFF; border-radius: 4px;"><i |
|||
class="fa fa-whatsapp mr-2"></i>WhatsApp</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
<!-- End of Contact Cards --> |
|||
</section> |
|||
<!-- Footer --> |
|||
<section class="oe_container" style="padding: 2rem 3rem 1rem;"> |
|||
<div class="row" style="max-width:1540px; margin: 0 auto; margin-right: 3rem; "> |
|||
<!-- Logo --> |
|||
<div class="col-lg-12 d-flex justify-content-center align-items-center" style="margin-top: 3rem;"> |
|||
<img src="https://www.cybrosys.com/images/logo.png" width="200px" height="auto" /> |
|||
</div> |
|||
<!-- End of Logo --> |
|||
<div class="col-lg-12"> |
|||
<hr |
|||
style="margin-top: 3rem;background: linear-gradient(90deg, rgba(2,0,36,0) 0%, rgba(229,229,229,1) 33%, rgba(229,229,229,1) 58%, rgba(0,212,255,0) 100%); height: 2px; border-style: none;"> |
|||
<!-- End of Footer Section --> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<!-- END OF FOOTER --> |
|||
|
|||
</div> |
@ -0,0 +1,21 @@ |
|||
/** @odoo-module */ |
|||
|
|||
import { registry } from "@web/core/registry"; |
|||
import { download } from "@web/core/network/download"; |
|||
import framework from 'web.framework'; |
|||
import session from 'web.session'; |
|||
|
|||
registry.category("ir.actions.report handlers").add("stock_xlsx", async (action) => { |
|||
if (action.report_type === 'stock_xlsx') { |
|||
framework.blockUI(); |
|||
var def = $.Deferred(); |
|||
session.get_file({ |
|||
url: '/xlsx_reports', |
|||
data: action.data, |
|||
success: def.resolve.bind(def), |
|||
error: (error) => this.call('crash_manager', 'rpc_error', error), |
|||
complete: framework.unblockUI, |
|||
}); |
|||
return def; |
|||
} |
|||
}); |
@ -0,0 +1,49 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
|
|||
<odoo> |
|||
<data> |
|||
<record model='ir.ui.view' id='wizard_form'> |
|||
<field name="name">wizard.stock.history.form</field> |
|||
<field name="model">wizard.stock.history</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Wizard"> |
|||
<group string="Warehouse"> |
|||
<field name="warehouse" widget="many2many_tags"/> |
|||
</group> |
|||
<notebook> |
|||
<page string="Category"> |
|||
<field name="category"> |
|||
<tree> |
|||
<field name="name"/> |
|||
</tree> |
|||
</field> |
|||
</page> |
|||
</notebook> |
|||
<footer> |
|||
<button name="export_xls" type="object" default_focus="1" |
|||
string="Export Product with Stock Info" class="oe_highlight" |
|||
context="{'xls_export':1}" icon="fa-download"/> |
|||
<button string="Cancel" class="oe_link" special="cancel" /> |
|||
</footer> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model='ir.actions.act_window' id='wizard_act'> |
|||
<field name="name">Export product stock in Excel</field> |
|||
<field name="res_model">wizard.stock.history</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="view_mode">form</field> |
|||
<field name="view_id" ref="wizard_form"/> |
|||
<field name="target">new</field> |
|||
</record> |
|||
|
|||
|
|||
<menuitem name="Current stock in Excel" |
|||
parent="stock.menu_warehouse_report" |
|||
id="export_excel" |
|||
action="wizard_act" |
|||
sequence="3"/> |
|||
</data> |
|||
|
|||
</odoo> |
@ -0,0 +1,39 @@ |
|||
Biometric Device Integration v15 |
|||
================================ |
|||
This Cybrosys's module integrates Odoo attendance with biometric device attendance. |
|||
|
|||
Features |
|||
======== |
|||
* Integrates biometric device(Face+Thumb) with HR attendance. |
|||
* Managing attendance automatically |
|||
* Keeps zk machine history in Odoo |
|||
* Option to configure multiple zk devices |
|||
* Option to clear all zk history from both device and Odoo |
|||
|
|||
Technical Notes |
|||
=============== |
|||
Used Libraries: |
|||
|
|||
*This integration is only applicable for the the device ZKteco model 'uFace 202' & 'iFace990' |
|||
* zklib |
|||
you can install zklib library using "sudo pip install zklib" |
|||
|
|||
Compatible Devices |
|||
|
|||
*ZKteco model 'uFace 202' |
|||
*ZKteco model 'iFace990' |
|||
|
|||
Author |
|||
======= |
|||
* Cybrosys Techno Solutions <https://www.cybrosys.com> |
|||
|
|||
Credits |
|||
======= |
|||
Developer: Niyas Raphy @ Cybrosys, odoo@cybrosys.com V11 |
|||
Developer: Jesni Banu @ cybrosys, odoo@cybrosys.com V10 |
|||
Developer: Basith @ Cybrosys, odoo@cybrosys.com V12 |
|||
Developer: Varsha Vivek @ Cybrosys, odoo@cybrosys.com V13 |
|||
Developer: Ijaz Ahammed @ Cybrosys, odoo@cybrosys.com V14 |
|||
Developer: Noushid Khan @ Cybrosys, odoo@cybrosys.com V15 |
|||
Developer: Mostafa Shokiel , mostafa.shokiel@gmail.com |
|||
|
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
from . import models |
@ -0,0 +1,43 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2021-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
{ |
|||
'name': 'Biometric Device Integration', |
|||
'version': '15.0.1.0.0', |
|||
'summary': """Integrating Biometric Device (Model: ZKteco uFace 202) With HR Attendance (Face + Thumb)""", |
|||
'description': """This module integrates Odoo with the biometric device(Model: ZKteco uFace 202),odoo15,odoo,hr,attendance""", |
|||
'category': 'Generic Modules/Human Resources', |
|||
'author': 'Cybrosys Techno Solutions, Mostafa Shokiel', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'website': "https://www.cybrosys.com", |
|||
'depends': ['base_setup', 'hr_attendance'], |
|||
'data': [ |
|||
'security/ir.model.access.csv', |
|||
'views/zk_machine_view.xml', |
|||
'views/zk_machine_attendance_view.xml', |
|||
'data/download_data.xml' |
|||
], |
|||
'images': ['static/description/banner.png'], |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': False, |
|||
} |
@ -0,0 +1,14 @@ |
|||
<?xml version="1.0"?> |
|||
<odoo noupdate="1"> |
|||
<record forcecreate="True" id="cron_download_data" model="ir.cron"> |
|||
<field name="name">Download Data</field> |
|||
<field eval="True" name="active"/> |
|||
<field name="user_id" ref="base.user_admin"/> |
|||
<field name="interval_number">10</field> |
|||
<field name="interval_type">minutes</field> |
|||
<field name="numbercall">-1</field> |
|||
<field name="model_id" ref="hr_zk_attendance.model_zk_machine"/> |
|||
<field name="state">code</field> |
|||
<field name="code">model.cron_download()</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,7 @@ |
|||
## Module <hr_zk_attendance> |
|||
|
|||
#### 03.10.2021 |
|||
#### Version 15.0.1.0.0 |
|||
##### ADD |
|||
- Initial commit |
|||
|
@ -0,0 +1,25 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
from . import zk_machine |
|||
from . import machine_analysis |
|||
from . import zklib |
|||
|
@ -0,0 +1,106 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
from odoo import tools |
|||
from odoo import models, fields, api, _ |
|||
|
|||
|
|||
class HrEmployee(models.Model): |
|||
_inherit = 'hr.employee' |
|||
|
|||
device_id = fields.Char(string='Biometric Device ID') |
|||
|
|||
|
|||
class ZkMachine(models.Model): |
|||
_name = 'zk.machine.attendance' |
|||
_inherit = 'hr.attendance' |
|||
|
|||
@api.constrains('check_in', 'check_out', 'employee_id') |
|||
def _check_validity(self): |
|||
"""overriding the __check_validity function for employee attendance.""" |
|||
pass |
|||
|
|||
device_id = fields.Char(string='Biometric Device ID') |
|||
punch_type = fields.Selection([('0', 'Check In'), |
|||
('1', 'Check Out'), |
|||
('2', 'Break Out'), |
|||
('3', 'Break In'), |
|||
('4', 'Overtime In'), |
|||
('5', 'Overtime Out')], |
|||
string='Punching Type') |
|||
|
|||
attendance_type = fields.Selection([('1', 'Finger'), |
|||
('15', 'Face'), |
|||
('2','Type_2'), |
|||
('3','Password'), |
|||
('4','Card')], string='Category') |
|||
punching_time = fields.Datetime(string='Punching Time') |
|||
address_id = fields.Many2one('res.partner', string='Working Address') |
|||
|
|||
|
|||
class ReportZkDevice(models.Model): |
|||
_name = 'zk.report.daily.attendance' |
|||
_auto = False |
|||
_order = 'punching_day desc' |
|||
|
|||
name = fields.Many2one('hr.employee', string='Employee') |
|||
punching_day = fields.Datetime(string='Date') |
|||
address_id = fields.Many2one('res.partner', string='Working Address') |
|||
attendance_type = fields.Selection([('1', 'Finger'), |
|||
('15', 'Face'), |
|||
('2','Type_2'), |
|||
('3','Password'), |
|||
('4','Card')], |
|||
string='Category') |
|||
punch_type = fields.Selection([('0', 'Check In'), |
|||
('1', 'Check Out'), |
|||
('2', 'Break Out'), |
|||
('3', 'Break In'), |
|||
('4', 'Overtime In'), |
|||
('5', 'Overtime Out')], string='Punching Type') |
|||
punching_time = fields.Datetime(string='Punching Time') |
|||
|
|||
def init(self): |
|||
tools.drop_view_if_exists(self._cr, 'zk_report_daily_attendance') |
|||
query = """ |
|||
create or replace view zk_report_daily_attendance as ( |
|||
select |
|||
min(z.id) as id, |
|||
z.employee_id as name, |
|||
z.write_date as punching_day, |
|||
z.address_id as address_id, |
|||
z.attendance_type as attendance_type, |
|||
z.punching_time as punching_time, |
|||
z.punch_type as punch_type |
|||
from zk_machine_attendance z |
|||
join hr_employee e on (z.employee_id=e.id) |
|||
GROUP BY |
|||
z.employee_id, |
|||
z.write_date, |
|||
z.address_id, |
|||
z.attendance_type, |
|||
z.punch_type, |
|||
z.punching_time |
|||
) |
|||
""" |
|||
self._cr.execute(query) |
|||
|
|||
|
@ -0,0 +1,208 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2020-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
import pytz |
|||
import sys |
|||
import datetime |
|||
import logging |
|||
import binascii |
|||
|
|||
from . import zklib |
|||
from .zkconst import * |
|||
from struct import unpack |
|||
from odoo import api, fields, models |
|||
from odoo import _ |
|||
from odoo.exceptions import UserError, ValidationError |
|||
_logger = logging.getLogger(__name__) |
|||
try: |
|||
from zk import ZK, const |
|||
except ImportError: |
|||
_logger.error("Please Install pyzk library.") |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class HrAttendance(models.Model): |
|||
_inherit = 'hr.attendance' |
|||
|
|||
device_id = fields.Char(string='Biometric Device ID') |
|||
|
|||
|
|||
class ZkMachine(models.Model): |
|||
_name = 'zk.machine' |
|||
|
|||
name = fields.Char(string='Machine IP', required=True) |
|||
port_no = fields.Integer(string='Port No', required=True) |
|||
address_id = fields.Many2one('res.partner', string='Working Address') |
|||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id.id) |
|||
|
|||
def device_connect(self, zk): |
|||
try: |
|||
conn = zk.connect() |
|||
return conn |
|||
except: |
|||
return False |
|||
|
|||
def clear_attendance(self): |
|||
for info in self: |
|||
try: |
|||
machine_ip = info.name |
|||
zk_port = info.port_no |
|||
timeout = 30 |
|||
try: |
|||
zk = ZK(machine_ip, port=zk_port, timeout=timeout, password=0, force_udp=False, ommit_ping=False) |
|||
except NameError: |
|||
raise UserError(_("Please install it with 'pip3 install pyzk'.")) |
|||
conn = self.device_connect(zk) |
|||
if conn: |
|||
conn.enable_device() |
|||
clear_data = zk.get_attendance() |
|||
if clear_data: |
|||
# conn.clear_attendance() |
|||
self._cr.execute("""delete from zk_machine_attendance""") |
|||
conn.disconnect() |
|||
raise UserError(_('Attendance Records Deleted.')) |
|||
else: |
|||
raise UserError(_('Unable to clear Attendance log. Are you sure attendance log is not empty.')) |
|||
else: |
|||
raise UserError( |
|||
_('Unable to connect to Attendance Device. Please use Test Connection button to verify.')) |
|||
except: |
|||
raise ValidationError( |
|||
'Unable to clear Attendance log. Are you sure attendance device is connected & record is not empty.') |
|||
|
|||
def getSizeUser(self, zk): |
|||
"""Checks a returned packet to see if it returned CMD_PREPARE_DATA, |
|||
indicating that data packets are to be sent |
|||
|
|||
Returns the amount of bytes that are going to be sent""" |
|||
command = unpack('HHHH', zk.data_recv[:8])[0] |
|||
if command == CMD_PREPARE_DATA: |
|||
size = unpack('I', zk.data_recv[8:12])[0] |
|||
print("size", size) |
|||
return size |
|||
else: |
|||
return False |
|||
|
|||
def zkgetuser(self, zk): |
|||
"""Start a connection with the time clock""" |
|||
try: |
|||
users = zk.get_users() |
|||
print(users) |
|||
return users |
|||
except: |
|||
return False |
|||
|
|||
@api.model |
|||
def cron_download(self): |
|||
machines = self.env['zk.machine'].search([]) |
|||
for machine in machines : |
|||
machine.download_attendance() |
|||
|
|||
def download_attendance(self): |
|||
_logger.info("++++++++++++Cron Executed++++++++++++++++++++++") |
|||
zk_attendance = self.env['zk.machine.attendance'] |
|||
att_obj = self.env['hr.attendance'] |
|||
for info in self: |
|||
machine_ip = info.name |
|||
zk_port = info.port_no |
|||
timeout = 15 |
|||
try: |
|||
zk = ZK(machine_ip, port=zk_port, timeout=timeout, password=0, force_udp=False, ommit_ping=False) |
|||
except NameError: |
|||
raise UserError(_("Pyzk module not Found. Please install it with 'pip3 install pyzk'.")) |
|||
conn = self.device_connect(zk) |
|||
if conn: |
|||
# conn.disable_device() #Device Cannot be used during this time. |
|||
try: |
|||
user = conn.get_users() |
|||
except: |
|||
user = False |
|||
try: |
|||
attendance = conn.get_attendance() |
|||
except: |
|||
attendance = False |
|||
if attendance: |
|||
for each in attendance: |
|||
atten_time = each.timestamp |
|||
atten_time = datetime.strptime(atten_time.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S') |
|||
local_tz = pytz.timezone( |
|||
self.env.user.partner_id.tz or 'GMT') |
|||
local_dt = local_tz.localize(atten_time, is_dst=None) |
|||
utc_dt = local_dt.astimezone(pytz.utc) |
|||
utc_dt = utc_dt.strftime("%Y-%m-%d %H:%M:%S") |
|||
atten_time = datetime.strptime( |
|||
utc_dt, "%Y-%m-%d %H:%M:%S") |
|||
atten_time = fields.Datetime.to_string(atten_time) |
|||
if user: |
|||
for uid in user: |
|||
if uid.user_id == each.user_id: |
|||
get_user_id = self.env['hr.employee'].search( |
|||
[('device_id', '=', each.user_id)]) |
|||
if get_user_id: |
|||
duplicate_atten_ids = zk_attendance.search( |
|||
[('device_id', '=', each.user_id), ('punching_time', '=', atten_time)]) |
|||
if duplicate_atten_ids: |
|||
continue |
|||
else: |
|||
zk_attendance.create({'employee_id': get_user_id.id, |
|||
'device_id': each.user_id, |
|||
'attendance_type': str(each.status), |
|||
'punch_type': str(each.punch), |
|||
'punching_time': atten_time, |
|||
'address_id': info.address_id.id}) |
|||
att_var = att_obj.search([('employee_id', '=', get_user_id.id), |
|||
('check_out', '=', False)]) |
|||
print('ddfcd', str(each.status)) |
|||
if each.punch == 0: #check-in |
|||
if not att_var: |
|||
att_obj.create({'employee_id': get_user_id.id, |
|||
'check_in': atten_time}) |
|||
if each.punch == 1: #check-out |
|||
if len(att_var) == 1: |
|||
att_var.write({'check_out': atten_time}) |
|||
else: |
|||
att_var1 = att_obj.search([('employee_id', '=', get_user_id.id)]) |
|||
if att_var1: |
|||
att_var1[-1].write({'check_out': atten_time}) |
|||
|
|||
else: |
|||
print('ddfcd', str(each.status)) |
|||
print('user', uid.name) |
|||
employee = self.env['hr.employee'].create( |
|||
{'device_id': each.user_id, 'name': uid.name}) |
|||
zk_attendance.create({'employee_id': employee.id, |
|||
'device_id': each.user_id, |
|||
'attendance_type': str(each.status), |
|||
'punch_type': str(each.punch), |
|||
'punching_time': atten_time, |
|||
'address_id': info.address_id.id}) |
|||
att_obj.create({'employee_id': employee.id, |
|||
'check_in': atten_time}) |
|||
else: |
|||
pass |
|||
# zk.enableDevice() |
|||
conn.disconnect |
|||
return True |
|||
else: |
|||
raise UserError(_('Unable to get the attendance log, please try again later.')) |
|||
else: |
|||
raise UserError(_('Unable to connect, please check the parameters and network connections.')) |
@ -0,0 +1,118 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
import binascii |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def getSizeAttendance(self): |
|||
"""Checks a returned packet to see if it returned CMD_PREPARE_DATA, |
|||
indicating that data packets are to be sent |
|||
|
|||
Returns the amount of bytes that are going to be sent""" |
|||
command = unpack('HHHH', self.data_recv[:8])[0] |
|||
if command == CMD_PREPARE_DATA: |
|||
size = unpack('I', self.data_recv[8:12])[0] |
|||
return size |
|||
else: |
|||
return False |
|||
|
|||
|
|||
def reverseHex(hexstr): |
|||
tmp = '' |
|||
for i in reversed(range(int(len(hexstr)/2))): |
|||
tmp += hexstr[i*2:(i*2)+2] |
|||
|
|||
return tmp |
|||
|
|||
def zkgetattendance(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_ATTLOG_RRQ |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
|
|||
if getSizeAttendance(self): |
|||
bytes = getSizeAttendance(self) |
|||
while bytes > 0: |
|||
data_recv, addr = self.zkclient.recvfrom(1032) |
|||
self.attendancedata.append(data_recv) |
|||
bytes -= 1024 |
|||
|
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
data_recv = self.zkclient.recvfrom(8) |
|||
|
|||
attendance = [] |
|||
if len(self.attendancedata) > 0: |
|||
# The first 4 bytes don't seem to be related to the user |
|||
for x in range(len(self.attendancedata)): |
|||
if x > 0: |
|||
self.attendancedata[x] = self.attendancedata[x][8:] |
|||
|
|||
attendancedata = b''.join( self.attendancedata ) |
|||
|
|||
attendancedata = attendancedata[14:] |
|||
|
|||
while len(attendancedata) > 40: |
|||
|
|||
uid, state, timestamp, space = unpack( '24s1s4s11s', attendancedata.ljust(40)[:40] ) |
|||
|
|||
|
|||
# Clean up some messy characters from the user name |
|||
#uid = unicode(uid.strip('\x00|\x01\x10x'), errors='ignore') |
|||
uid = uid.split(b'\x00', 1)[0].decode('utf-8') |
|||
#print "%s, %s, %s" % (uid, state, decode_time( int( reverseHex( timestamp.encode('hex') ), 16 ) ) ) |
|||
|
|||
attendance.append( ( uid, int( binascii.hexlify(state), 16 ), decode_time( int( reverseHex( binascii.hexlify(timestamp).decode('utf-8')), 16 ) ) ) ) |
|||
|
|||
attendancedata = attendancedata[40:] |
|||
|
|||
return attendance |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkclearattendance(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_CLEAR_ATTLOG |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
@ -0,0 +1,63 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkconnect(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_CONNECT |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = 0 |
|||
reply_id = -1 + USHRT_MAX |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
|
|||
self.zkclient.sendto(buf, self.address) |
|||
|
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
|
|||
return self.checkValid( self.data_recv ) |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkdisconnect(self): |
|||
"""Disconnect from the clock""" |
|||
command = CMD_EXIT |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
|
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
|
|||
self.zkclient.sendto(buf, self.address) |
|||
|
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
return self.checkValid( self.data_recv ) |
@ -0,0 +1,71 @@ |
|||
from datetime import datetime, date |
|||
|
|||
USHRT_MAX = 65535 |
|||
|
|||
|
|||
CMD_CONNECT = 1000 |
|||
CMD_EXIT = 1001 |
|||
CMD_ENABLEDEVICE = 1002 |
|||
CMD_DISABLEDEVICE = 1003 |
|||
|
|||
CMD_ACK_OK = 2000 |
|||
CMD_ACK_ERROR = 2001 |
|||
CMD_ACK_DATA = 2002 |
|||
|
|||
CMD_PREPARE_DATA = 1500 |
|||
CMD_DATA = 1501 |
|||
|
|||
CMD_USERTEMP_RRQ = 9 |
|||
CMD_ATTLOG_RRQ = 13 |
|||
CMD_CLEAR_DATA = 14 |
|||
CMD_CLEAR_ATTLOG = 15 |
|||
|
|||
CMD_WRITE_LCD = 66 |
|||
|
|||
CMD_GET_TIME = 201 |
|||
CMD_SET_TIME = 202 |
|||
|
|||
CMD_VERSION = 1100 |
|||
CMD_DEVICE = 11 |
|||
|
|||
CMD_CLEAR_ADMIN = 20 |
|||
CMD_SET_USER = 8 |
|||
|
|||
LEVEL_USER = 0 |
|||
LEVEL_ADMIN = 14 |
|||
|
|||
def encode_time(t): |
|||
"""Encode a timestamp send at the timeclock |
|||
|
|||
copied from zkemsdk.c - EncodeTime""" |
|||
d = ( (t.year % 100) * 12 * 31 + ((t.month - 1) * 31) + t.day - 1) *\ |
|||
(24 * 60 * 60) + (t.hour * 60 + t.minute) * 60 + t.second |
|||
|
|||
return d |
|||
|
|||
|
|||
def decode_time(t): |
|||
"""Decode a timestamp retrieved from the timeclock |
|||
|
|||
copied from zkemsdk.c - DecodeTime""" |
|||
second = t % 60 |
|||
t = t / 60 |
|||
|
|||
minute = t % 60 |
|||
t = t / 60 |
|||
|
|||
hour = t % 24 |
|||
t = t / 24 |
|||
|
|||
day = t % 31+1 |
|||
t = t / 31 |
|||
|
|||
month = t % 12+1 |
|||
t = t / 12 |
|||
|
|||
year = t + 2000 |
|||
|
|||
d = datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) |
|||
|
|||
return d |
|||
|
@ -0,0 +1,82 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkdevicename(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~DeviceName' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkenabledevice(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_ENABLEDEVICE |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|||
def zkdisabledevice(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DISABLEDEVICE |
|||
command_string = '\x00\x00' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
@ -0,0 +1,56 @@ |
|||
def zkextendfmt(self): |
|||
try: |
|||
test = self.exttrynumber |
|||
except: |
|||
self.exttrynumber = 1 |
|||
|
|||
data_seq=[ self.data_recv.encode("hex")[4:6], self.data_recv.encode("hex")[6:8] ] |
|||
#print data_seq |
|||
if self.exttrynumber == 1: |
|||
plus1 = 0 |
|||
plus2 = 0 |
|||
else: |
|||
plus1 = -1 |
|||
plus2 = +1 |
|||
|
|||
|
|||
desc = ": +"+hex( int('99', 16)+plus1 ).lstrip('0x')+", +"+hex(int('b1', 16)+plus2).lstrip("0x") |
|||
self.data_seq1 = hex( int( data_seq[0], 16 ) + int( '99', 16 ) + plus1 ).lstrip("0x") |
|||
self.data_seq2 = hex( int( data_seq[1], 16 ) + int( 'b1', 16 ) + plus2 ).lstrip("0x") |
|||
|
|||
if len(self.data_seq1) >= 3: |
|||
#self.data_seq2 = hex( int( self.data_seq2, 16 ) + int( self.data_seq1[:1], 16) ).lstrip("0x") |
|||
self.data_seq1 = self.data_seq1[-2:] |
|||
|
|||
if len(self.data_seq2) >= 3: |
|||
#self.data_seq1 = hex( int( self.data_seq1, 16 ) + int( self.data_seq2[:1], 16) ).lstrip("0x") |
|||
self.data_seq2 = self.data_seq2[-2:] |
|||
|
|||
|
|||
if len(self.data_seq1) <= 1: |
|||
self.data_seq1 = "0"+self.data_seq1 |
|||
|
|||
if len(self.data_seq2) <= 1: |
|||
self.data_seq2 = "0"+self.data_seq2 |
|||
|
|||
|
|||
counter = hex( self.counter ).lstrip("0x") |
|||
if len(counter): |
|||
counter = "0" + counter |
|||
#print self.data_seq1+" "+self.data_seq2+desc |
|||
data = "0b00"+self.data_seq1+self.data_seq2+self.id_com+counter+"007e457874656e64466d7400" |
|||
self.zkclient.sendto(data.decode("hex"), self.address) |
|||
#print data |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
except: |
|||
if self.exttrynumber == 1: |
|||
self.exttrynumber = 2 |
|||
tmp = zkextendfmt(self) |
|||
if len(tmp) < 1: |
|||
self.exttrynumber = 1 |
|||
|
|||
self.id_com = self.data_recv.encode("hex")[8:12] |
|||
self.counter = self.counter+1 |
|||
#print self.data_recv.encode("hex") |
|||
return self.data_recv[8:] |
@ -0,0 +1,61 @@ |
|||
def zkextendoplog(self, index=0): |
|||
try: |
|||
test = self.extlogtrynumber |
|||
except: |
|||
self.extlogtrynumber = 1 |
|||
|
|||
data_seq = [ self.data_recv.encode("hex")[4:6], self.data_recv.encode("hex")[6:8] ] |
|||
|
|||
if index==0: |
|||
self.data_seq1 = hex( int( data_seq[0], 16 ) + int( '104', 16 ) ).lstrip("0x") |
|||
self.data_seq2 = hex( int( data_seq[1], 16 ) + int( '19', 16 ) ).lstrip("0x") |
|||
desc = ": +104, +19" |
|||
header="0b00" |
|||
elif index==1: |
|||
self.data_seq1 = hex( abs( int( data_seq[0], 16 ) - int( '2c', 16 ) ) ).lstrip("0x") |
|||
self.data_seq2 = hex( abs( int( data_seq[1], 16 ) - int( '2', 16 ) ) ).lstrip("0x") |
|||
desc = ": -2c, -2" |
|||
header="d107" |
|||
elif index>=2: |
|||
self.data_seq1 = hex( abs( int( data_seq[0], 16 ) - int( '2c', 16 ) ) ).lstrip("0x") |
|||
self.data_seq2 = hex( abs( int( data_seq[1], 16 ) - int( '2', 16 ) ) ).lstrip("0x") |
|||
desc = ": -2c, -2" |
|||
header="ffff" |
|||
|
|||
|
|||
#print self.data_seq1+" "+self.data_seq2 |
|||
if len(self.data_seq1) >= 3: |
|||
self.data_seq2 = hex( int( self.data_seq2, 16 ) + int( self.data_seq1[:1], 16) ).lstrip("0x") |
|||
self.data_seq1 = self.data_seq1[-2:] |
|||
|
|||
if len(self.data_seq2) >= 3: |
|||
self.data_seq1 = hex( int( self.data_seq1, 16 ) + int( self.data_seq2[:1], 16) ).lstrip("0x") |
|||
self.data_seq2 = self.data_seq2[-2:] |
|||
|
|||
if len(self.data_seq1) <= 1: |
|||
self.data_seq1 = "0"+self.data_seq1 |
|||
|
|||
if len(self.data_seq2) <= 1: |
|||
self.data_seq2 = "0"+self.data_seq2 |
|||
|
|||
|
|||
counter = hex( self.counter ).lstrip("0x") |
|||
if len(counter): |
|||
counter = "0" + counter |
|||
|
|||
#print self.data_seq1+" "+self.data_seq2+desc |
|||
data = header+self.data_seq1+self.data_seq2+self.id_com+counter+"00457874656e644f504c6f6700" |
|||
self.zkclient.sendto(data.decode("hex"), self.address) |
|||
#print data |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
except: |
|||
bingung=1 |
|||
if self.extlogtrynumber == 1: |
|||
self.extlogtrynumber = 2 |
|||
zkextendoplog(self) |
|||
|
|||
self.id_com = self.data_recv.encode("hex")[8:12] |
|||
self.counter = self.counter+1 |
|||
#print self.data_recv.encode("hex") |
|||
return self.data_recv[8:] |
@ -0,0 +1,44 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# Copyright (C) 2018-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: cybrosys(<https://www.cybrosys.com>) |
|||
# |
|||
# This program is free software: you can modify |
|||
# it under the terms of the GNU Affero General Public License (AGPL) as |
|||
# published by the Free Software Foundation, either version 3 of the |
|||
# License, or (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Affero General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Affero General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################### |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkfaceon(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = 'FaceFunOn' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
@ -0,0 +1,156 @@ |
|||
from socket import * |
|||
from .zkconnect import * |
|||
from .zkversion import * |
|||
from .zkos import * |
|||
from .zkextendfmt import * |
|||
from .zkextendoplog import * |
|||
from .zkplatform import * |
|||
from .zkworkcode import * |
|||
from .zkssr import * |
|||
from .zkpin import * |
|||
from .zkface import * |
|||
from .zkserialnumber import * |
|||
from .zkdevice import * |
|||
from .zkuser import * |
|||
from .zkattendance import * |
|||
from .zktime import * |
|||
|
|||
class ZKLib: |
|||
|
|||
def __init__(self, ip, port): |
|||
self.address = (ip, port) |
|||
self.zkclient = socket(AF_INET, SOCK_DGRAM) |
|||
self.zkclient.settimeout(3) |
|||
self.session_id = 0 |
|||
self.userdata = [] |
|||
self.attendancedata = [] |
|||
|
|||
|
|||
def createChkSum(self, p): |
|||
"""This function calculates the chksum of the packet to be sent to the |
|||
time clock |
|||
|
|||
Copied from zkemsdk.c""" |
|||
l = len(p) |
|||
chksum = 0 |
|||
while l > 1: |
|||
chksum += unpack('H', pack('BB', p[0], p[1]))[0] |
|||
|
|||
p = p[2:] |
|||
if chksum > USHRT_MAX: |
|||
chksum -= USHRT_MAX |
|||
l -= 2 |
|||
|
|||
|
|||
if l: |
|||
chksum = chksum + p[-1] |
|||
|
|||
while chksum > USHRT_MAX: |
|||
chksum -= USHRT_MAX |
|||
|
|||
chksum = ~chksum |
|||
|
|||
while chksum < 0: |
|||
chksum += USHRT_MAX |
|||
|
|||
return pack('H', chksum) |
|||
|
|||
|
|||
def createHeader(self, command, chksum, session_id, reply_id, |
|||
command_string): |
|||
"""This function puts a the parts that make up a packet together and |
|||
packs them into a byte string""" |
|||
buf = pack('HHHH', command, chksum, session_id, reply_id) + command_string.encode(encoding='utf_8', errors='strict') |
|||
|
|||
buf = unpack('8B'+'%sB' % len(command_string), buf) |
|||
|
|||
chksum = unpack('H', self.createChkSum(buf))[0] |
|||
#print unpack('H', self.createChkSum(buf)) |
|||
reply_id += 1 |
|||
if reply_id >= USHRT_MAX: |
|||
reply_id -= USHRT_MAX |
|||
|
|||
buf = pack('HHHH', command, chksum, session_id, reply_id) |
|||
return buf + command_string.encode(encoding='utf_8', errors='strict') |
|||
|
|||
|
|||
def checkValid(self, reply): |
|||
"""Checks a returned packet to see if it returned CMD_ACK_OK, |
|||
indicating success""" |
|||
command = unpack('HHHH', reply[:8])[0] |
|||
if command == CMD_ACK_OK: |
|||
return True |
|||
else: |
|||
return False |
|||
|
|||
def connect(self): |
|||
return zkconnect(self) |
|||
|
|||
def disconnect(self): |
|||
return zkdisconnect(self) |
|||
|
|||
def version(self): |
|||
return zkversion(self) |
|||
|
|||
def osversion(self): |
|||
return zkos(self) |
|||
|
|||
def extendFormat(self): |
|||
return zkextendfmt(self) |
|||
|
|||
def extendOPLog(self, index=0): |
|||
return zkextendoplog(self, index) |
|||
|
|||
def platform(self): |
|||
return zkplatform(self) |
|||
|
|||
def fmVersion(self): |
|||
return zkplatformVersion(self) |
|||
|
|||
def workCode(self): |
|||
return zkworkcode(self) |
|||
|
|||
def ssr(self): |
|||
return zkssr(self) |
|||
|
|||
def pinWidth(self): |
|||
return zkpinwidth(self) |
|||
|
|||
def faceFunctionOn(self): |
|||
return zkfaceon(self) |
|||
|
|||
def serialNumber(self): |
|||
return zkserialnumber(self) |
|||
|
|||
def deviceName(self): |
|||
return zkdevicename(self) |
|||
|
|||
def disableDevice(self): |
|||
return zkdisabledevice(self) |
|||
|
|||
def enableDevice(self): |
|||
return zkenabledevice(self) |
|||
|
|||
def getUser(self): |
|||
return zkgetuser(self) |
|||
|
|||
def setUser(self, uid, userid, name, password, role): |
|||
return zksetuser(self, uid, userid, name, password, role) |
|||
|
|||
def clearUser(self): |
|||
return zkclearuser(self) |
|||
|
|||
def clearAdmin(self): |
|||
return zkclearadmin(self) |
|||
|
|||
def getAttendance(self): |
|||
return zkgetattendance(self) |
|||
|
|||
def clearAttendance(self): |
|||
return zkclearattendance(self) |
|||
|
|||
def setTime(self, t): |
|||
return zksettime(self, t) |
|||
|
|||
def getTime(self): |
|||
return zkgettime(self) |
@ -0,0 +1,23 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkos(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~OS' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
|
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
|
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
@ -0,0 +1,23 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkpinwidth(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~PIN2Width' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
@ -0,0 +1,43 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkplatform(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~Platform' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkplatformVersion(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~ZKFPVersion' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
@ -0,0 +1,23 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkserialnumber(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~SerialNumber' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
@ -0,0 +1,23 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkssr(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = '~SSR' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
@ -0,0 +1,50 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def reverseHex(hexstr): |
|||
tmp = '' |
|||
for i in reversed(range(len(hexstr)/2)): |
|||
tmp += hexstr[i*2:(i*2)+2] |
|||
|
|||
return tmp |
|||
|
|||
|
|||
def zksettime(self, t): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_SET_TIME |
|||
command_string = pack('I',encode_time(t)) |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkgettime(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_GET_TIME |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return decode_time( int( reverseHex( self.data_recv[8:].encode("hex") ), 16 ) ) |
|||
except: |
|||
return False |
@ -0,0 +1,140 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def getSizeUser(self): |
|||
"""Checks a returned packet to see if it returned CMD_PREPARE_DATA, |
|||
indicating that data packets are to be sent |
|||
|
|||
Returns the amount of bytes that are going to be sent""" |
|||
command = unpack('HHHH', self.data_recv[:8])[0] |
|||
if command == CMD_PREPARE_DATA: |
|||
size = unpack('I', self.data_recv[8:12])[0] |
|||
return size |
|||
else: |
|||
return False |
|||
|
|||
|
|||
def zksetuser(self, uid, userid, name, password, role): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_SET_USER |
|||
command_string = pack('sxs8s28ss7sx8s16s', chr( uid ), chr(role), password, name, chr(1), '', userid, '' ) |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkgetuser(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_USERTEMP_RRQ |
|||
command_string = '\x05' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
|
|||
|
|||
if getSizeUser(self): |
|||
bytes = getSizeUser(self) |
|||
|
|||
while bytes > 0: |
|||
data_recv, addr = self.zkclient.recvfrom(1032) |
|||
self.userdata.append(data_recv) |
|||
bytes -= 1024 |
|||
|
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
data_recv = self.zkclient.recvfrom(8) |
|||
|
|||
users = {} |
|||
if len(self.userdata) > 0: |
|||
# The first 4 bytes don't seem to be related to the user |
|||
for x in range(len(self.userdata)): |
|||
if x > 0: |
|||
self.userdata[x] = self.userdata[x][8:] |
|||
|
|||
userdata = ''.join( self.userdata ) |
|||
|
|||
userdata = userdata[11:] |
|||
|
|||
while len(userdata) > 72: |
|||
|
|||
uid, role, password, name, userid = unpack( '2s2s8s28sx31s', userdata.ljust(72)[:72] ) |
|||
|
|||
uid = int( uid.encode("hex"), 16) |
|||
# Clean up some messy characters from the user name |
|||
password = password.split('\x00', 1)[0] |
|||
password = unicode(password.strip('\x00|\x01\x10x'), errors='ignore') |
|||
|
|||
#uid = uid.split('\x00', 1)[0] |
|||
userid = unicode(userid.strip('\x00|\x01\x10x'), errors='ignore') |
|||
|
|||
name = name.split('\x00', 1)[0] |
|||
|
|||
if name.strip() == "": |
|||
name = uid |
|||
|
|||
users[uid] = (userid, name, int( role.encode("hex"), 16 ), password) |
|||
|
|||
#print("%d, %s, %s, %s, %s" % (uid, userid, name, int( role.encode("hex"), 16 ), password)) |
|||
userdata = userdata[72:] |
|||
|
|||
return users |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkclearuser(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_CLEAR_DATA |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|||
|
|||
def zkclearadmin(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_CLEAR_ADMIN |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
@ -0,0 +1,23 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkversion(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_VERSION |
|||
command_string = '' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
@ -0,0 +1,23 @@ |
|||
from struct import pack, unpack |
|||
from .zkconst import * |
|||
|
|||
|
|||
def zkworkcode(self): |
|||
"""Start a connection with the time clock""" |
|||
command = CMD_DEVICE |
|||
command_string = 'WorkCode' |
|||
chksum = 0 |
|||
session_id = self.session_id |
|||
reply_id = unpack('HHHH', self.data_recv[:8])[3] |
|||
|
|||
buf = self.createHeader(command, chksum, session_id, |
|||
reply_id, command_string) |
|||
self.zkclient.sendto(buf, self.address) |
|||
#print buf.encode("hex") |
|||
try: |
|||
self.data_recv, addr = self.zkclient.recvfrom(1024) |
|||
self.session_id = unpack('HHHH', self.data_recv[:8])[2] |
|||
return self.data_recv[8:] |
|||
except: |
|||
return False |
|||
|
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 204 KiB |
After Width: | Height: | Size: 350 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 44 KiB |