mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'new/12.0/pos_pax' into 12.0-test
This commit is contained in:
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
'name': 'USA - Arkansas - Payroll',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Localization',
|
||||
'depends': ['l10n_us_hr_payroll'],
|
||||
'version': '12.0.2019.0.0',
|
||||
'description': """
|
||||
USA - Arkansas Payroll Rules.
|
||||
==================================
|
||||
|
||||
* Contribution register and partner for Arkansas Department of Financial Administration (ADFA) - Income Tax Withholding
|
||||
* Contribution register and partner for Arkansas Department of Workforce Solutions (ADWS) - Unemployment
|
||||
* Contract level Arkansas Exemptions
|
||||
* Company level Arkansas Unemployment Rate
|
||||
* Salary Structure for Arkansas
|
||||
""",
|
||||
'auto_install': False,
|
||||
'website': 'https://hibou.io/',
|
||||
'data': [
|
||||
'views/hr_payroll_views.xml',
|
||||
'data/base.xml',
|
||||
'data/rates.xml',
|
||||
'data/rules.xml',
|
||||
'data/final.xml',
|
||||
],
|
||||
'installable': True
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- PARTNERS -->
|
||||
<record id="res_partner_ar_dws_unemp" model="res.partner">
|
||||
<field name="name">Arkansas Department of Workforce Solutions - Unemployment Tax</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
<record id="res_partner_ar_dfa_withhold" model="res.partner">
|
||||
<field name="name">Arkansas Department of Financial Administration - Income Tax Withholding</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
|
||||
<!-- CONTRIBUTION REGISTERS -->
|
||||
<record id="contrib_register_ar_dws_unemp" model="hr.contribution.register">
|
||||
<field name="name">Arkansas Unemployment</field>
|
||||
<field name="note">Arkansas Department of Workforce Solutions - Unemployment</field>
|
||||
<field name="partner_id" ref="res_partner_ar_dws_unemp"/>
|
||||
</record>
|
||||
<record id="contrib_register_ar_dfa_withhold" model="hr.contribution.register">
|
||||
<field name="name">Arkansas Income Tax Withholding</field>
|
||||
<field name="note">Arkansas Department of Financial Administration - Income Tax Withholding</field>
|
||||
<field name="partner_id" ref="res_partner_ar_dfa_withhold"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- HR SALARY RULE CATEGORIES-->
|
||||
<record id="hr_payroll_ar_unemp_wages" model="hr.salary.rule.category">
|
||||
<field name="name">Wage: US-AR Unemployment</field>
|
||||
<field name="code">WAGE_US_AR_UNEMP</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_ar_unemp" model="hr.salary.rule.category">
|
||||
<field name="name">ER: US-AR Unemployment</field>
|
||||
<field name="code">ER_US_AR_UNEMP</field>
|
||||
<field name="parent_id" ref="hr_payroll.COMP"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_ar_income_withhold" model="hr.salary.rule.category">
|
||||
<field name="name">EE: US-AR Income Tax Withholding</field>
|
||||
<field name="code">EE_US_AR_INC_WITHHOLD</field>
|
||||
<field name="parent_id" ref="hr_payroll.DED"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- HR PAYROLL STRUCTURE -->
|
||||
<record id="hr_payroll_salary_structure_us_ar_employee" model="hr.payroll.structure">
|
||||
<field name="code">US_AR_EMP</field>
|
||||
<field name="name">USA Arkansas Employee</field>
|
||||
<field eval="[(6, 0, [
|
||||
ref('hr_payroll_rules_ar_unemp_wages'),
|
||||
ref('hr_payroll_rules_ar_unemp'),
|
||||
ref('hr_payroll_rules_ar_inc_withhold'),
|
||||
])]" name="rule_ids"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="parent_id" ref="l10n_us_hr_payroll.hr_payroll_salary_structure_us_employee"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="hr_payroll_rates_ar_unemp" model="hr.payroll.rate">
|
||||
<field name="name">US AR Unemployment</field>
|
||||
<field name="code">US_AR_UNEMP</field>
|
||||
<field name="rate">3.2</field>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="wage_limit_year" eval="10000.00"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,113 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_payroll_rules_ar_unemp_wages" model="hr.salary.rule">
|
||||
<field name="sequence" eval="423"/>
|
||||
<field name="category_id" ref="hr_payroll_ar_unemp_wages"/>
|
||||
<field name="name">Wage: US-AR Unemployment</field>
|
||||
<field name="code">WAGE_US_AR_UNEMP</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (contract.futa_type != contract.FUTA_TYPE_BASIC)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
rate = payslip.dict.get_rate('US_AR_UNEMP')
|
||||
year = payslip.dict.date_to.year
|
||||
ytd = payslip.sum('WAGE_US_AR_UNEMP', str(year) + '-01-01', str(year+1) + '-01-01')
|
||||
ytd += contract.external_wages
|
||||
remaining = rate.wage_limit_year - ytd
|
||||
if remaining <= 0.0:
|
||||
result = 0
|
||||
elif remaining < categories.BASIC:
|
||||
result = remaining
|
||||
else:
|
||||
result = categories.BASIC
|
||||
</field>
|
||||
<field name="appears_on_payslip" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_rules_ar_unemp" model="hr.salary.rule">
|
||||
<field name="sequence" eval="443"/>
|
||||
<field name="category_id" ref="hr_payroll_ar_unemp"/>
|
||||
<field name="name">ER: US-AR Unemployment</field>
|
||||
<field name="code">ER_US_AR_UNEMP</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (contract.futa_type != contract.FUTA_TYPE_BASIC)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
rate = payslip.dict.get_rate('US_AR_UNEMP')
|
||||
result_rate = -rate.rate
|
||||
result = categories.WAGE_US_AR_UNEMP
|
||||
|
||||
# result_rate of 0 implies 100% due to bug
|
||||
if result_rate == 0.0:
|
||||
result = 0.0
|
||||
</field>
|
||||
<field name="register_id" ref="contrib_register_ar_dws_unemp"/>
|
||||
<field name="appears_on_payslip" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_rules_ar_inc_withhold" model="hr.salary.rule">
|
||||
<field name="sequence" eval="155"/>
|
||||
<field name="category_id" ref="hr_payroll_ar_income_withhold"/>
|
||||
<field name="name">EE: US-AR Income Tax Withholding</field>
|
||||
<field name="code">EE_US_AR_INC_WITHHOLD</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = not (contract.ar_ar4ec_texarkana_exemption or contract.ar_ar4ec_tax_exempt)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
wages = categories.GROSS
|
||||
annual_gross_pay = 0.00
|
||||
allowance_amt = contract.ar_ar4ec_allowances * 26.00
|
||||
schedule_pay = contract.schedule_pay
|
||||
standard_deduction = 2200
|
||||
additional_withholding = contract.ar_ar4ec_additional_wh
|
||||
|
||||
if contract.w4_filing_status == 'married':
|
||||
standard_deduction = standard_deduction * 2
|
||||
|
||||
pay_period = 0.0
|
||||
pay_periods = {
|
||||
'weekly': 52.0,
|
||||
'bi-weekly': 26.0,
|
||||
'semi-monthly': 24.0,
|
||||
'monthly': 12.0
|
||||
}
|
||||
if schedule_pay in pay_periods:
|
||||
pay_period = pay_periods[schedule_pay]
|
||||
else:
|
||||
raise Exception('Invalid schedule_pay="' + schedule_pay + '" for AR Income Withholding calculation')
|
||||
|
||||
annual_gross_pay = (wages * pay_period)
|
||||
net_taxable_income = annual_gross_pay - standard_deduction - allowance_amt
|
||||
if (net_taxable_income < 50000.00):
|
||||
# This formula will round the number to the nearest 50 if under 50000
|
||||
net_taxable_income = (net_taxable_income // 50) * 50.0 + 50.0
|
||||
|
||||
tax_rate_table = [(4299, 0.90),
|
||||
(8499, 2.50),
|
||||
(12699, 3.50),
|
||||
(21199, 4.50),
|
||||
(35099, 6.0),
|
||||
(float('inf'), 6.9)]
|
||||
|
||||
result = 0.0
|
||||
last = 0.0
|
||||
|
||||
for row in tax_rate_table:
|
||||
cap, rate = row
|
||||
if cap <= net_taxable_income:
|
||||
taxed = cap - last
|
||||
result = result + (taxed * (rate / 100.0))
|
||||
last = cap
|
||||
elif cap > net_taxable_income:
|
||||
taxed = net_taxable_income - last
|
||||
result = result + (taxed * (rate / 100.0))
|
||||
break
|
||||
|
||||
result = (result / pay_period) + additional_withholding
|
||||
result = -result
|
||||
</field>
|
||||
<field name="register_id" ref="contrib_register_ar_dfa_withhold"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import hr_payroll
|
||||
@@ -1,14 +0,0 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class USARHrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
|
||||
ar_ar4ec_allowances = fields.Integer(string='Arkansas AR-4EC Allowances',
|
||||
oldname='ar_w4_allowances')
|
||||
ar_ar4ec_additional_wh = fields.Float(string="Arkansas AR-4EC Additional Withholding",
|
||||
oldname='ar_w4_additional_wh')
|
||||
ar_ar4ec_tax_exempt = fields.Boolean(string='Arkansas AR-4EC Tax Exempt',
|
||||
oldname='ar_w4_tax_exempt')
|
||||
ar_ar4ec_texarkana_exemption = fields.Boolean(string='Arkansas AR-4EC Texarkana Exemption',
|
||||
oldname='ar_w4_texarkana_exemption')
|
||||
@@ -1 +0,0 @@
|
||||
from . import test_us_ar_payslip_2019
|
||||
@@ -1,291 +0,0 @@
|
||||
from odoo.addons.l10n_us_hr_payroll.tests.test_us_payslip import TestUsPayslip, process_payslip
|
||||
|
||||
|
||||
class TestUsARPayslip(TestUsPayslip):
|
||||
|
||||
AR_UNEMP_MAX_WAGE = 10000.00
|
||||
AR_UNEMP = -3.2 / 100.0
|
||||
AR_INC_TAX = -0.0535
|
||||
|
||||
def test_taxes_monthly(self):
|
||||
salary = 10000.0
|
||||
schedule_pay = 'monthly'
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, salary,
|
||||
struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
|
||||
self.assertEqual(contract.schedule_pay, 'monthly')
|
||||
|
||||
self._log('2019 Arkansas tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
# Not exempt from rule 1 or rule 2 - unemployment wages., and actual unemployment.
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], salary)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
# Make a new payslip, this one will have maximums
|
||||
remaining_ar_unemp_wages = self.AR_UNEMP_MAX_WAGE - salary if (self.AR_UNEMP_MAX_WAGE - 2*salary < salary) \
|
||||
else salary
|
||||
# We reached the cap of 10000.0 in the first payslip.
|
||||
self.assertEqual(0.0, remaining_ar_unemp_wages)
|
||||
self._log('2019 Arkansas tax second payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], remaining_ar_unemp_wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], remaining_ar_unemp_wages * self.AR_UNEMP)
|
||||
|
||||
def test_taxes_with_state_exempt(self):
|
||||
salary = 50000.0
|
||||
tax_exempt = True # State withholding should be zero.
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
salary,
|
||||
struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
)
|
||||
contract.ar_ar4ec_tax_exempt = tax_exempt
|
||||
|
||||
self._log('2019 Arkansas exempt tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], self.AR_UNEMP_MAX_WAGE)
|
||||
self.assertPayrollEqual(cats.get('ER_US_AR_UNEMP', 0.0), cats.get('WAGE_US_AR_UNEMP', 0.0) * self.AR_UNEMP)
|
||||
self.assertPayrollEqual(cats.get('EE_US_AR_INC_WITHHOLD', 0.0), 0.0)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
def test_taxes_with_texarkana_exempt(self):
|
||||
salary = 40000.00
|
||||
texarkana_exemption = True # State withholding should be zero.
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
salary,
|
||||
struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'))
|
||||
contract.ar_ar4ec_texarkana_exemption = texarkana_exemption
|
||||
|
||||
self._log('2019 Arkansas tax first payslip:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats.get('WAGE_US_AR_UNEMP', 0.0), self.AR_UNEMP_MAX_WAGE)
|
||||
self.assertPayrollEqual(cats.get('ER_US_AR_UNEMP', 0.0), cats.get('WAGE_US_AR_UNEMP', 0.0) * self.AR_UNEMP)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
def test_additional_withholding(self):
|
||||
wages = 5000.0
|
||||
schedule_pay = 'monthly'
|
||||
pay_periods = 12
|
||||
additional_wh = 150.0
|
||||
exemptions = 2
|
||||
# TODO: comment on how it was calculated
|
||||
test_ar_amt = 3069.97
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
wages,
|
||||
struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ar_ar4ec_additional_wh = 0.0
|
||||
contract.ar_ar4ec_allowances = exemptions
|
||||
|
||||
self.assertEqual(contract.schedule_pay, 'monthly')
|
||||
|
||||
self._log('2019 Arkansas tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP)
|
||||
# TODO: change to hand the test_ar_amt already be divided by pay periods
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods))
|
||||
|
||||
contract.ar_ar4ec_additional_wh = additional_wh
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
def test_under_fifty_thousand(self):
|
||||
wages = 2500.00
|
||||
schedule_pay = 'monthly'
|
||||
pay_periods = 12
|
||||
additional_wh = 150.0
|
||||
exemptions = 2
|
||||
# TODO: comment calc.
|
||||
test_ar_amt = 1066.151
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
wages,
|
||||
struct_id=self.ref(
|
||||
'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ar_ar4ec_additional_wh = 0.0
|
||||
contract.ar_ar4ec_allowances = exemptions
|
||||
|
||||
self.assertEqual(contract.schedule_pay, 'monthly')
|
||||
|
||||
self._log('2019 Arkansas tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods))
|
||||
|
||||
contract.ar_ar4ec_additional_wh = additional_wh
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
# Make a new payslip, this one will have maximums
|
||||
|
||||
remaining_ar_unemp_wages = self.AR_UNEMP_MAX_WAGE - wages if (self.AR_UNEMP_MAX_WAGE - 2 * wages < wages) \
|
||||
else wages
|
||||
|
||||
self._log('2019 Arkansas tax second payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], remaining_ar_unemp_wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], remaining_ar_unemp_wages * self.AR_UNEMP)
|
||||
|
||||
def test_over_fifty_thousand(self):
|
||||
wages = 10000.00 # 10000.00 monthly is over 50,000 annually.
|
||||
schedule_pay = 'monthly'
|
||||
pay_periods = 12
|
||||
additional_wh = 150.0
|
||||
exemptions = 2
|
||||
# TODO: comment on how it was calculated
|
||||
test_ar_amt = 7209.97
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
wages,
|
||||
struct_id=self.ref(
|
||||
'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ar_ar4ec_additional_wh = 0.0
|
||||
contract.ar_ar4ec_allowances = exemptions
|
||||
|
||||
self.assertEqual(contract.schedule_pay, 'monthly')
|
||||
|
||||
self._log('2019 Arkansas tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods))
|
||||
|
||||
contract.ar_ar4ec_additional_wh = additional_wh
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
# Make a new payslip, this one will have maximums
|
||||
|
||||
remaining_ar_unemp_wages = self.AR_UNEMP_MAX_WAGE - wages if (self.AR_UNEMP_MAX_WAGE - 2 * wages < wages) \
|
||||
else wages
|
||||
|
||||
self._log('2019 Arkansas tax second payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], remaining_ar_unemp_wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], remaining_ar_unemp_wages * self.AR_UNEMP)
|
||||
|
||||
def test_married(self):
|
||||
wages = 5500.00
|
||||
schedule_pay = 'monthly'
|
||||
pay_periods = 12
|
||||
additional_wh = 150.0
|
||||
exemptions = 2
|
||||
w4_filing_status = 'married'
|
||||
# TODO: explain calc.
|
||||
# Yearly -> 3332.17. Monthly -> 427.681
|
||||
test_ar_amt = 3332.17
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
wages,
|
||||
struct_id=self.ref(
|
||||
'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ar_ar4ec_additional_wh = additional_wh
|
||||
contract.ar_ar4ec_allowances = exemptions
|
||||
contract.w4_filing_status = w4_filing_status
|
||||
|
||||
self.assertEqual(contract.w4_filing_status, 'married')
|
||||
|
||||
self._log('2019 Arkansas tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh)
|
||||
|
||||
def test_single(self):
|
||||
wages = 5500.00
|
||||
schedule_pay = 'monthly'
|
||||
pay_periods = 12
|
||||
additional_wh = 150.0
|
||||
exemptions = 2
|
||||
w4_filling_status = 'single'
|
||||
# TODO: explain calc.
|
||||
# Yearly -> 3483.972 Monthly -> 298.331
|
||||
test_ar_amt = 3483.972
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
wages,
|
||||
struct_id=self.ref(
|
||||
'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ar_ar4ec_additional_wh = 0
|
||||
contract.ar_ar4ec_allowances = exemptions
|
||||
contract.w4_filling_status = w4_filling_status
|
||||
|
||||
self.assertEqual(contract.w4_filling_status, 'single')
|
||||
|
||||
self._log('2019 Arkansas tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods))
|
||||
|
||||
contract.ar_ar4ec_additional_wh = additional_wh
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh)
|
||||
|
||||
process_payslip(payslip)
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="hr_contract_form_l10n_us_ar_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.contract.form.inherit</field>
|
||||
<field name="model">hr.contract</field>
|
||||
<field name="priority">106</field>
|
||||
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//group[@name='state_filing']" position="inside">
|
||||
<group string="Arkansas" name="ar">
|
||||
<field name="ar_ar4ec_allowances" string="Allowances"/>
|
||||
<field name="ar_ar4ec_additional_wh" string="Additional Withholding"/>
|
||||
<field name="ar_ar4ec_tax_exempt" string="Tax Exempt"/>
|
||||
<field name="ar_ar4ec_texarkana_exemption" string="Texarkana Exemption"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import models
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
'name': 'USA - Iowa - Payroll',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Localization',
|
||||
'depends': ['l10n_us_hr_payroll'],
|
||||
'version': '12.0.2019.0.0',
|
||||
'description': """
|
||||
USA - Iowa Payroll Rules.
|
||||
==================================
|
||||
|
||||
* Contribution register and partner for Iowa Workforce Development (IWD) - Unemployment
|
||||
* Contribution register and partner for Iowa Department of Revenue (IDOR) - Income Tax Withholding
|
||||
* Contract level Iowa Exemptions
|
||||
* Company level Iowa Unemployment Rate
|
||||
* Salary Structure for Iowa
|
||||
""",
|
||||
|
||||
'auto_install': False,
|
||||
'website': 'https://hibou.io/',
|
||||
'data': [
|
||||
'views/hr_payroll_views.xml',
|
||||
'data/base.xml',
|
||||
'data/rates.xml',
|
||||
'data/rules.xml',
|
||||
'data/final.xml',
|
||||
],
|
||||
'installable': True
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- PARTNERS -->
|
||||
<record id="res_partner_ia_wd_unemp" model="res.partner">
|
||||
<field name="name">Iowa Workforce Development- Unemployment Tax</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
<record id="res_partner_ia_dor_withhold" model="res.partner">
|
||||
<field name="name">Iowa Department of Revenue - Income Tax Withholding</field>
|
||||
<field name="supplier">1</field>
|
||||
<field eval="0" name="customer"/>
|
||||
</record>
|
||||
|
||||
<!-- CONTRIBUTION REGISTERS -->
|
||||
<record id="contrib_register_ia_wd_unemp" model="hr.contribution.register">
|
||||
<field name="name">Iowa Unemployment</field>
|
||||
<field name="note">Iowa Workforce Development - Unemployment</field>
|
||||
<field name="partner_id" ref="res_partner_ia_wd_unemp"/>
|
||||
</record>
|
||||
<record id="contrib_register_ia_dor_withhold" model="hr.contribution.register">
|
||||
<field name="name">Iowa Income Tax Withholding</field>
|
||||
<field name="note">Iowa Department of Revenue - Income Tax Withholding</field>
|
||||
<field name="partner_id" ref="res_partner_ia_dor_withhold"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- HR SALARY RULE CATEGORIES-->
|
||||
<record id="hr_payroll_ia_unemp_wages" model="hr.salary.rule.category">
|
||||
<field name="name">Wage: US-IA Unemployment</field>
|
||||
<field name="code">WAGE_US_IA_UNEMP</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_ia_unemp" model="hr.salary.rule.category">
|
||||
<field name="name">ER: US-IA Unemployment</field>
|
||||
<field name="code">ER_US_IA_UNEMP</field>
|
||||
<field name="parent_id" ref="hr_payroll.COMP"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_ia_income_withhold" model="hr.salary.rule.category">
|
||||
<field name="name">EE: US-IA Income Tax Withholding</field>
|
||||
<field name="code">EE_US_IA_INC_WITHHOLD</field>
|
||||
<field name="parent_id" ref="hr_payroll.DED"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- HR PAYROLL STRUCTURE -->
|
||||
<record id="hr_payroll_salary_structure_us_ia_employee" model="hr.payroll.structure">
|
||||
<field name="code">US_IA_EMP</field>
|
||||
<field name="name">USA Iowa Employee</field>
|
||||
<field eval="[(6, 0, [
|
||||
ref('hr_payroll_rules_ia_unemp_wages'),
|
||||
ref('hr_payroll_rules_ia_unemp'),
|
||||
ref('hr_payroll_rules_ia_inc_withhold'),
|
||||
])]" name="rule_ids"/>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="parent_id" ref="l10n_us_hr_payroll.hr_payroll_salary_structure_us_employee"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="hr_payroll_rates_ia_unemp" model="hr.payroll.rate">
|
||||
<field name="name">US IA Unemployment</field>
|
||||
<field name="code">US_IA_UNEMP</field>
|
||||
<field name="rate">1.0</field>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="wage_limit_year" eval="30600.00"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,160 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- HR SALARY RULES-->
|
||||
<record id="hr_payroll_rules_ia_unemp_wages" model="hr.salary.rule">
|
||||
<field name="sequence" eval="423"/>
|
||||
<field name="category_id" ref="hr_payroll_ia_unemp_wages"/>
|
||||
<field name="name">Wage: US-IA Unemployment</field>
|
||||
<field name="code">WAGE_US_IA_UNEMP</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (contract.futa_type != contract.FUTA_TYPE_BASIC)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
rate = payslip.dict.get_rate('US_IA_UNEMP')
|
||||
year = payslip.dict.date_to.year
|
||||
ytd = payslip.sum('WAGE_US_IA_UNEMP', str(year) + '-01-01', str(year+1) + '-01-01')
|
||||
ytd += contract.external_wages
|
||||
remaining = rate.wage_limit_year - ytd
|
||||
if remaining <= 0.0:
|
||||
result = 0
|
||||
elif remaining < categories.BASIC:
|
||||
result = remaining
|
||||
else:
|
||||
result = categories.BASIC
|
||||
</field>
|
||||
<field name="appears_on_payslip" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_rules_ia_unemp" model="hr.salary.rule">
|
||||
<field name="sequence" eval="443"/>
|
||||
<field name="category_id" ref="hr_payroll_ia_unemp"/>
|
||||
<field name="name">ER: US-IA Unemployment</field>
|
||||
<field name="code">ER_US_IA_UNEMP</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (contract.futa_type != contract.FUTA_TYPE_BASIC)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
rate = payslip.dict.get_rate('US_IA_UNEMP')
|
||||
result_rate = -rate.rate
|
||||
result = categories.WAGE_US_IA_UNEMP
|
||||
|
||||
# result_rate of 0 implies 100% due to bug
|
||||
if result_rate == 0.0:
|
||||
result = 0.0
|
||||
</field>
|
||||
<field name="register_id" ref="contrib_register_ia_wd_unemp"/>
|
||||
<field name="appears_on_payslip" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="hr_payroll_rules_ia_inc_withhold" model="hr.salary.rule">
|
||||
<field name="sequence" eval="155"/>
|
||||
<field name="category_id" ref="hr_payroll_ia_income_withhold"/>
|
||||
<field name="name">EE: US-IA Income Tax Withholding</field>
|
||||
<field name="code">EE_US_IA_INC_WITHHOLD</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = not contract.ia_w4_tax_exempt</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
wages = categories.GROSS
|
||||
federal_withholding = categories.EE_US_FED_INC_WITHHOLD
|
||||
schedule_pay = contract.schedule_pay
|
||||
allowances = contract.ia_w4_allowances
|
||||
# It is + federal_withholding because federal_withholding is negative.
|
||||
t1 = wages + federal_withholding
|
||||
standard_deduction_table = {
|
||||
'daily': (6.50, 16.00),
|
||||
'weekly': (32.50, 80.00),
|
||||
'bi-weekly': (65.00, 160.00),
|
||||
'semi-monthly': (70.42, 173.33),
|
||||
'monthly': (140.83, 346.67),
|
||||
'annually': (1690.00, 4160.00)}
|
||||
t2 = t1 - standard_deduction_table[schedule_pay][0] if (allowances < 2) else standard_deduction_table[schedule_pay][1]
|
||||
# IMPORTANT -- ALL RATES ARE ALREADY DIVIDED BY 100 -> 8.53% is in the table as 0.0853
|
||||
if schedule_pay == 'weekly':
|
||||
tax_rate_table = [
|
||||
(25.63, 0.0033, 0.0),
|
||||
(51.27, 0.0067, 0.08),
|
||||
(102.52, 0.0225, 0.025),
|
||||
(230.67, 0.0414, 1.40),
|
||||
(384.46, 0.0563, 6.71),
|
||||
(512.62, 0.0596, 15.37),
|
||||
(768.92, 0.0625, 23.01),
|
||||
(1153.38, 0.0744, 39.03),
|
||||
(float('inf'), 0.0853, 67.63),
|
||||
]
|
||||
elif schedule_pay == 'bi-weekly':
|
||||
tax_rate_table = [
|
||||
(51.27, 0.0033, 0.00),
|
||||
(102.54, 0.0067, 0.17),
|
||||
(205.04, 0.00225, 0.51),
|
||||
(461.35, 0.0414, 2.82),
|
||||
(768.92, 0.0563, 13.43),
|
||||
(1025.23, 0.0596, 30.75),
|
||||
(1537.85, 0.0625, 46.03),
|
||||
(2306.77, 0.0744, 78.07),
|
||||
(float('inf'), 0.0853, 135.28)
|
||||
]
|
||||
elif schedule_pay == 'semi-monthly':
|
||||
tax_rate_table = [
|
||||
(55.54, 0.0033, 0.00),
|
||||
(111.08, 0.0067, 0.18),
|
||||
(222.13, 0.0225, 0.55),
|
||||
(499.79, 0.0414, 3.05),
|
||||
(833.00, 0.0563, 14.59),
|
||||
(1110.67, 0.0596, 33.31),
|
||||
(1666.00, 0.0625, 49.86),
|
||||
(2499.00, 0.0744, 84.57),
|
||||
(float('inf'), 0.0853, 146.55)
|
||||
]
|
||||
elif schedule_pay == 'monthly':
|
||||
tax_rate_table = [
|
||||
(111.08, 0.0033, 0.00),
|
||||
(222.17, 0.0067, 0.37),
|
||||
(444.25, 0.0225, 1.11),
|
||||
(999.58, 0.0414, 6.11),
|
||||
(1666.00, 0.0563, 29.10),
|
||||
(2221.33, 0.0596, 62.66),
|
||||
(3332.00, 0.0625, 99.72),
|
||||
(4998.00, 0.0744, 169.14),
|
||||
(float('inf'), 0.0853, 293.09)
|
||||
]
|
||||
elif schedule_pay == 'annual':
|
||||
tax_rate_table = [
|
||||
(1333.00, 0.0033, 0.00),
|
||||
(2666.00, 0.0067, 4.40),
|
||||
(5331.00, 0.0225, 13.33),
|
||||
(11995.00, 0.0414, 73.29),
|
||||
(19992.00, 0.0563, 349.19),
|
||||
(26656.00, 0.0596, 799.41),
|
||||
(39984.00, 0.0625, 1196.58),
|
||||
(59976.00, 0.0744, 2029.58),
|
||||
(float('inf'), 0.0853, 3516.98)
|
||||
]
|
||||
|
||||
t3 = 0.0
|
||||
last = 0.0
|
||||
for row in tax_rate_table:
|
||||
cap, rate, flat_fee = row
|
||||
if cap > t2:
|
||||
taxed_amount = t2 - last
|
||||
t3 = flat_fee + (rate * taxed_amount)
|
||||
break
|
||||
last = cap
|
||||
|
||||
deduction_per_allowance = {
|
||||
'daily': 0.15,
|
||||
'weekly': 0.77,
|
||||
'bi-weekly': 1.54,
|
||||
'semi-monthly': 1.67,
|
||||
'monthly': 3.33,
|
||||
'annually': 40.00,
|
||||
}
|
||||
t4 = t3 - (deduction_per_allowance[schedule_pay] * allowances)
|
||||
t5 = t4 + contract.ia_w4_additional_wh
|
||||
result = -t5
|
||||
</field>
|
||||
<field name="register_id" ref="contrib_register_ia_dor_withhold"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import hr_payroll
|
||||
@@ -1,9 +0,0 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class USIAHrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
|
||||
ia_w4_allowances = fields.Integer(string='Iowa W-4 allowances')
|
||||
ia_w4_additional_wh = fields.Float(string="Iowa W-4 Additional Withholding")
|
||||
ia_w4_tax_exempt = fields.Boolean(string="Iowa W-4 Tax Exempt")
|
||||
@@ -1 +0,0 @@
|
||||
from . import test_us_ia_payslip
|
||||
@@ -1,182 +0,0 @@
|
||||
from odoo.addons.l10n_us_hr_payroll.tests.test_us_payslip import TestUsPayslip, process_payslip
|
||||
|
||||
|
||||
class TestUsIAPayslip(TestUsPayslip):
|
||||
IA_UNEMP_MAX_WAGE = 30600
|
||||
IA_UNEMP = -1.0 / 100.0
|
||||
IA_INC_TAX = -0.0535
|
||||
|
||||
def test_taxes_weekly(self):
|
||||
wages = 30000.00
|
||||
schedule_pay = 'weekly'
|
||||
allowances = 1
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wages,
|
||||
struct_id=self.ref('l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ia_w4_allowances = allowances
|
||||
|
||||
self.assertEqual(contract.schedule_pay, 'weekly')
|
||||
|
||||
self._log('2019 Iowa tax first payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
# T1 is the gross taxable wages for the pay period minus the Federal withholding amount. We add the federal
|
||||
# withholding amount because it is calculated in the base US payroll module as a negative
|
||||
# t1 = 30000 - (10399.66) = 19600.34
|
||||
t1_to_test = wages + cats['EE_US_FED_INC_WITHHOLD']
|
||||
# T2 is T1 minus our standard deduction which is a table of flat rates dependent on the number of allowances.
|
||||
# In our case, we have a weekly period which on the table has a std deduct. of $32.50 for 0 or 1 allowances,
|
||||
# and 80.00 of 2 or more allowances.
|
||||
standard_deduction = 32.50 # The allowance tells us what standard_deduction amount to use.
|
||||
# t2 = 19600.34 - 32.50 = 19567.84
|
||||
t2_to_test = t1_to_test - standard_deduction
|
||||
# T3 is T2 multiplied by the income rates in the large table plus a flat fee for that bracket.
|
||||
# 1153.38 is the bracket floor. 8.53 is the rate, and 67.63 is the flat fee.
|
||||
# t3 = 1638.38
|
||||
t3_to_test = ((t2_to_test - 1153.38) * (8.53 / 100)) + 67.63
|
||||
# T4 is T3 minus a flat amount determined by pay period * the number of deductions. For 2019, our weekly
|
||||
# deduction amount per allowance is 0.77
|
||||
# t4 = 1638.38 - 0.77 = 155.03
|
||||
t4_to_test = t3_to_test - (0.77 * allowances)
|
||||
# t5 is our T4 plus the additional withholding per period
|
||||
# t5 = 1637.61 + 0.0
|
||||
# Convert to negative as well.
|
||||
t5_to_test = -t4_to_test
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test)
|
||||
|
||||
# Test additional
|
||||
additional_wh = 15.00
|
||||
contract.ia_w4_additional_wh = additional_wh
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test - additional_wh)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
# Make a new payslip, this one will have maximums
|
||||
|
||||
remaining_ia_unemp_wages = self.IA_UNEMP_MAX_WAGE - wages if (self.IA_UNEMP_MAX_WAGE - 2*wages < wages) \
|
||||
else wages
|
||||
|
||||
self._log('2019 Iowa tax second payslip weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], remaining_ia_unemp_wages)
|
||||
self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], remaining_ia_unemp_wages * self.IA_UNEMP)
|
||||
|
||||
def test_taxes_biweekly(self):
|
||||
wages = 3000.00
|
||||
schedule_pay = 'bi-weekly'
|
||||
allowances = 1
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wages,
|
||||
struct_id=self.ref(
|
||||
'l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ia_w4_allowances = allowances
|
||||
|
||||
self.assertEqual(contract.schedule_pay, 'bi-weekly')
|
||||
|
||||
self._log('2019 Iowa tax first payslip bi-weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
# T1 is the gross taxable wages for the pay period minus the Federal withholding amount. We add the federal
|
||||
# withholding amount because it is calculated in the base US payroll module as a negative
|
||||
t1_to_test = wages + cats['EE_US_FED_INC_WITHHOLD']
|
||||
# T2 is T1 minus our standard deduction which is a table of flat rates dependent on the number of allowances.
|
||||
# In our case, we have a biweekly period which on the table has a std deduct. of $65.00 for 0 or 1 allowances,
|
||||
# and $160.00 of 2 or more allowances.
|
||||
standard_deduction = 65.00 # The allowance tells us what standard_deduction amount to use.
|
||||
t2_to_test = t1_to_test - standard_deduction
|
||||
# T3 is T2 multiplied by the income rates in the large table plus a flat fee for that bracket.
|
||||
t3_to_test = ((t2_to_test - 2306.77) * (8.53 / 100)) + 135.28
|
||||
# T4 is T3 minus a flat amount determined by pay period * the number of deductions. For 2019, our weekly
|
||||
# deduction amount per allowance is 0.77
|
||||
t4_to_test = t3_to_test - (1.54 * allowances)
|
||||
# t5 is our T4 plus the additional withholding per period
|
||||
t5_to_test = -t4_to_test
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
def test_taxes_with_external_weekly(self):
|
||||
wages = 2500.00
|
||||
schedule_pay = 'weekly'
|
||||
allowances = 1
|
||||
additional_wh = 0.00
|
||||
external_wages = 500.0
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee, wages, external_wages=external_wages,
|
||||
struct_id=self.ref('l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ia_w4_additional_wh = additional_wh
|
||||
contract.ia_w4_allowances = allowances
|
||||
|
||||
self._log('2019 Iowa external tax first payslip external weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
|
||||
# T1 is the gross taxable wages for the pay period minus the Federal withholding amount. We add the federal
|
||||
# withholding amount because it is calculated in the base US payroll module as a negative
|
||||
t1_to_test = wages + cats['EE_US_FED_INC_WITHHOLD']
|
||||
# T2 is T1 minus our standard deduction which is a table of flat rates dependent on the number of allowances.
|
||||
# In our case, we have a weekly period which on the table has a std deduct. of $32.50 for 0 or 1 allowances,
|
||||
# and 80.00 of 2 or more allowances.
|
||||
standard_deduction = 32.50 # The allowance tells us what standard_deduction amount to use.
|
||||
t2_to_test = t1_to_test - standard_deduction
|
||||
# T3 is T2 multiplied by the income rates in the large table plus a flat fee for that bracket.
|
||||
t3_to_test = ((t2_to_test - 1153.38) * (8.53 / 100)) + 67.63
|
||||
# T4 is T3 minus a flat amount determined by pay period * the number of deductions. For 2019, our weekly
|
||||
# deduction amount per allowance is 0.77
|
||||
t4_to_test = t3_to_test - (0.77 * allowances)
|
||||
# t5 is our T4 plus the additional withholding per period
|
||||
t5_to_test = -t4_to_test - additional_wh
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], wages)
|
||||
self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP)
|
||||
self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test)
|
||||
|
||||
process_payslip(payslip)
|
||||
|
||||
def test_taxes_with_state_exempt_weekly(self):
|
||||
salary = 5000.0
|
||||
external_wages = 10000.0
|
||||
schedule_pay = 'weekly'
|
||||
|
||||
employee = self._createEmployee()
|
||||
contract = self._createContract(employee,
|
||||
salary,
|
||||
external_wages=external_wages,
|
||||
struct_id=self.ref('l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'),
|
||||
schedule_pay=schedule_pay)
|
||||
contract.ia_w4_tax_exempt = True
|
||||
|
||||
self._log('2019 Iowa exempt tax first payslip exempt weekly:')
|
||||
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
|
||||
payslip.compute_sheet()
|
||||
cats = self._getCategories(payslip)
|
||||
|
||||
self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], salary)
|
||||
self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP)
|
||||
self.assertPayrollEqual(cats.get('EE_US_IA_INC_WITHHOLD', 0.00), 0.00)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<data>
|
||||
<record id="hr_contract_form_l10n_us_ia_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.contract.form.inherit</field>
|
||||
<field name="model">hr.contract</field>
|
||||
<field name="priority">119</field>
|
||||
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<data>
|
||||
<xpath expr="//group[@name='state_filing']" position="inside">
|
||||
<group string="Iowa" name="ia">
|
||||
<field name="ia_w4_allowances" string="Allowances"/>
|
||||
<field name="ia_w4_additional_wh" string="Additional Withholding"/>
|
||||
<field name="ia_w4_tax_exempt" string="Tax Exempt"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
3
pos_pax/__init__.py
Normal file
3
pos_pax/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
43
pos_pax/__manifest__.py
Normal file
43
pos_pax/__manifest__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'POS PAX Terminal Credit Card',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Point of Sale',
|
||||
'sequence': 6,
|
||||
'summary': 'PAX Terminal Credit card support for Point Of Sale',
|
||||
'description': """
|
||||
Allow credit card POS payments
|
||||
==============================
|
||||
|
||||
This module allows customers to pay for their orders with credit cards.
|
||||
The transactions are processed on the PAX Terminal (no credit credit card
|
||||
information through Odoo itself).
|
||||
|
||||
Depending on Device and processor support, this integration can handle:
|
||||
|
||||
* Magnetic swiped cards
|
||||
* EMV chip cards
|
||||
* Contactless (including Apple Pay, Samsung Pay, Google Pay)
|
||||
|
||||
""",
|
||||
'depends': [
|
||||
'web',
|
||||
'pos_sale',
|
||||
# 'hibou_professional',
|
||||
],
|
||||
'website': 'https://hibou.io',
|
||||
'data': [
|
||||
'views/pos_pax_templates.xml',
|
||||
'views/pos_pax_views.xml',
|
||||
'views/pos_config_setting_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/pos_pax.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
4
pos_pax/models/__init__.py
Normal file
4
pos_pax/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import pos_pax
|
||||
from . import update
|
||||
61
pos_pax/models/pos_pax.py
Normal file
61
pos_pax/models/pos_pax.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = "account.bank.statement.line"
|
||||
|
||||
pax_card_number = fields.Char(string='Card Number', help='Masked credit card.')
|
||||
pax_txn_id = fields.Char(string='PAX Transaction ID')
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
pos_use_pax = fields.Boolean(string='Use POS PAX Terminal',
|
||||
help='When used in POS, communicate with PAX Terminal for transactions.')
|
||||
|
||||
|
||||
class PosConfig(models.Model):
|
||||
_inherit = 'pos.config'
|
||||
|
||||
pax_endpoint = fields.Char(string='PAX Endpoint',
|
||||
help='Endpoint for PAX device (include protocol (http or https) and port). '
|
||||
'e.g. http://192.168.1.101:10009')
|
||||
|
||||
|
||||
class PosOrder(models.Model):
|
||||
_inherit = "pos.order"
|
||||
|
||||
@api.model
|
||||
def _payment_fields(self, ui_paymentline):
|
||||
fields = super(PosOrder, self)._payment_fields(ui_paymentline)
|
||||
|
||||
fields.update({
|
||||
'pax_card_number': ui_paymentline.get('pax_card_number'),
|
||||
'pax_txn_id': ui_paymentline.get('pax_txn_id'),
|
||||
})
|
||||
|
||||
return fields
|
||||
|
||||
def add_payment(self, data):
|
||||
statement_id = super(PosOrder, self).add_payment(data)
|
||||
statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id),
|
||||
('pos_statement_id', '=', self.id),
|
||||
('journal_id', '=', data['journal'])])
|
||||
statement_lines = statement_lines.filtered(lambda line: float_compare(line.amount, data['amount'],
|
||||
precision_rounding=line.journal_currency_id.rounding) == 0)
|
||||
|
||||
# we can get multiple statement_lines when there are >1 credit
|
||||
# card payments with the same amount. In that case it doesn't
|
||||
# matter which statement line we pick, just pick one that
|
||||
# isn't already used.
|
||||
for line in statement_lines:
|
||||
if not line.pax_card_number:
|
||||
line.pax_card_number = data.get('pax_card_number')
|
||||
line.pax_txn_id = data.get('pax_txn_id')
|
||||
break
|
||||
|
||||
return statement_id
|
||||
20
pos_pax/models/update.py
Normal file
20
pos_pax/models/update.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class PublisherWarrantyContract(models.AbstractModel):
|
||||
_inherit = 'publisher_warranty.contract'
|
||||
|
||||
def _get_hibou_modules(self):
|
||||
modules = super(PublisherWarrantyContract, self)._get_hibou_modules()
|
||||
try:
|
||||
self.env.cr.execute(
|
||||
'SELECT COUNT(*) FROM pos_config WHERE pax_endpoint != \'\' AND pax_endpoint IS NOT NULL')
|
||||
pax_count = self.env.cr.fetchone()[0] or 0
|
||||
modules.update({
|
||||
'pos_pax': pax_count,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return modules
|
||||
17
pos_pax/static/src/css/pos_pax.css
Normal file
17
pos_pax/static/src/css/pos_pax.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.pos .paymentline.selected.o_pos_pax_txn_pending, .pos .paymentline.o_pos_pax_txn_pending {
|
||||
background: rgb(239, 153, 65);
|
||||
}
|
||||
.pos .col-tendered.edit.o_pos_pax_txn_pending {
|
||||
color: rgb(239, 153, 65);
|
||||
box-shadow: 0px 0px 0px 3px rgb(239, 153, 65);
|
||||
}
|
||||
|
||||
.pos .paymentline .pax_send_transaction {
|
||||
cursor: pointer !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255,255,255, 0.2);
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 6px 0 0 0;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
133
pos_pax/static/src/js/jquery_base64.js
vendored
Normal file
133
pos_pax/static/src/js/jquery_base64.js
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
odoo.define('pos_pax.jquery_base64', function (require) {
|
||||
"use strict";
|
||||
|
||||
// Hibou Corp. © 2020 - Wrapped library for PAX Device
|
||||
|
||||
var jQuery = require('jquery');
|
||||
|
||||
/*!
|
||||
* jquery.base64.js 0.1 - https://github.com/yckart/jquery.base64.js
|
||||
* Makes Base64 en & -decoding simpler as it is.
|
||||
*
|
||||
* Based upon: https://gist.github.com/Yaffle/1284012
|
||||
*
|
||||
* Copyright (c) 2012 Yannick Albert (http://yckart.com)
|
||||
* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
|
||||
* 2013/02/10
|
||||
**/
|
||||
;(function ($) {
|
||||
|
||||
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
|
||||
a256 = '',
|
||||
r64 = [256],
|
||||
r256 = [256],
|
||||
i = 0;
|
||||
|
||||
var UTF8 = {
|
||||
|
||||
/**
|
||||
* Encode multi-byte Unicode string into utf-8 multiple single-byte characters
|
||||
* (BMP / basic multilingual plane only)
|
||||
*
|
||||
* Chars in range U+0080 - U+07FF are encoded in 2 chars, U+0800 - U+FFFF in 3 chars
|
||||
*
|
||||
* @param {String} strUni Unicode string to be encoded as UTF-8
|
||||
* @returns {String} encoded string
|
||||
*/
|
||||
encode: function (strUni) {
|
||||
// use regular expressions & String.replace callback function for better efficiency
|
||||
// than procedural approaches
|
||||
var strUtf = strUni.replace(/[\u0080-\u07ff]/g, // U+0080 - U+07FF => 2 bytes 110yyyyy, 10zzzzzz
|
||||
function (c) {
|
||||
var cc = c.charCodeAt(0);
|
||||
return String.fromCharCode(0xc0 | cc >> 6, 0x80 | cc & 0x3f);
|
||||
})
|
||||
.replace(/[\u0800-\uffff]/g, // U+0800 - U+FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz
|
||||
function (c) {
|
||||
var cc = c.charCodeAt(0);
|
||||
return String.fromCharCode(0xe0 | cc >> 12, 0x80 | cc >> 6 & 0x3F, 0x80 | cc & 0x3f);
|
||||
});
|
||||
return strUtf;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decode utf-8 encoded string back into multi-byte Unicode characters
|
||||
*
|
||||
* @param {String} strUtf UTF-8 string to be decoded back to Unicode
|
||||
* @returns {String} decoded string
|
||||
*/
|
||||
decode: function (strUtf) {
|
||||
// note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
|
||||
var strUni = strUtf.replace(/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars
|
||||
function (c) { // (note parentheses for precence)
|
||||
var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f);
|
||||
return String.fromCharCode(cc);
|
||||
})
|
||||
.replace(/[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars
|
||||
function (c) { // (note parentheses for precence)
|
||||
var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f;
|
||||
return String.fromCharCode(cc);
|
||||
});
|
||||
return strUni;
|
||||
}
|
||||
};
|
||||
|
||||
while (i < 256) {
|
||||
var c = String.fromCharCode(i);
|
||||
a256 += c;
|
||||
r256[i] = i;
|
||||
r64[i] = b64.indexOf(c);
|
||||
++i;
|
||||
}
|
||||
|
||||
function code(s, discard, alpha, beta, w1, w2) {
|
||||
s = String(s);
|
||||
var buffer = 0,
|
||||
i = 0,
|
||||
length = s.length,
|
||||
result = '',
|
||||
bitsInBuffer = 0;
|
||||
|
||||
while (i < length) {
|
||||
var c = s.charCodeAt(i);
|
||||
c = c < 256 ? alpha[c] : -1;
|
||||
|
||||
buffer = (buffer << w1) + c;
|
||||
bitsInBuffer += w1;
|
||||
|
||||
while (bitsInBuffer >= w2) {
|
||||
bitsInBuffer -= w2;
|
||||
var tmp = buffer >> bitsInBuffer;
|
||||
result += beta.charAt(tmp);
|
||||
buffer ^= tmp << bitsInBuffer;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (!discard && bitsInBuffer > 0) result += beta.charAt(buffer << (w2 - bitsInBuffer));
|
||||
return result;
|
||||
}
|
||||
|
||||
var Plugin = $.base64 = function (dir, input, encode) {
|
||||
return input ? Plugin[dir](input, encode) : dir ? null : this;
|
||||
};
|
||||
|
||||
Plugin.btoa = Plugin.encode = function (plain, utf8encode) {
|
||||
plain = Plugin.raw === false || Plugin.utf8encode || utf8encode ? UTF8.encode(plain) : plain;
|
||||
plain = code(plain, false, r256, b64, 8, 6);
|
||||
return plain + '===='.slice((plain.length % 4) || 4);
|
||||
};
|
||||
|
||||
Plugin.atob = Plugin.decode = function (coded, utf8decode) {
|
||||
coded = String(coded).split('=');
|
||||
var i = coded.length;
|
||||
do {
|
||||
--i;
|
||||
coded[i] = code(coded[i], true, r64, a256, 6, 8);
|
||||
} while (i > 0);
|
||||
coded = coded.join('');
|
||||
return Plugin.raw === false || Plugin.utf8decode || utf8decode ? UTF8.decode(coded) : coded;
|
||||
};
|
||||
}(jQuery));
|
||||
|
||||
return jQuery;
|
||||
});
|
||||
479
pos_pax/static/src/js/pax_device.js
Normal file
479
pos_pax/static/src/js/pax_device.js
Normal file
@@ -0,0 +1,479 @@
|
||||
odoo.define('pos_pax.pax_device', function (require) {
|
||||
"use strict";
|
||||
|
||||
var $ = require('pos_pax.jquery_base64');
|
||||
|
||||
// Hibou Corp. © 2020 - Wrapped library for PAX Device
|
||||
// Additions and Fixes:
|
||||
// 2020-11-12 Fixed variable i and ii in getLRC
|
||||
// 2020-11-12 Fixed/added 'fail' mechanisms for XHR
|
||||
|
||||
//HEX TO BASE64
|
||||
function hexToBase64(str) {
|
||||
return $.base64.btoa(String.fromCharCode.apply(null,
|
||||
str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
|
||||
);
|
||||
}
|
||||
|
||||
//BASE64 TO HEX
|
||||
function base64ToHex(str) {
|
||||
for (var i = 0, bin = $.base64.atob(str), hex = []; i < bin.length; ++i) {
|
||||
var tmp = bin.charCodeAt(i).toString(16);
|
||||
if (tmp.length === 1) tmp = "0" + tmp;
|
||||
hex[hex.length] = tmp;
|
||||
}
|
||||
return hex.join(" ");
|
||||
}
|
||||
|
||||
function StringToHex(response){
|
||||
var responseHex = "";
|
||||
for(var i=0; i<response.length; i++){
|
||||
if(responseHex == "")
|
||||
responseHex = response.charCodeAt(i).toString(16).length<2?'0'+response.charCodeAt(i).toString(16):response.charCodeAt(i).toString(16);
|
||||
else
|
||||
responseHex += response.charCodeAt(i).toString(16).length<2?" " + '0'+response.charCodeAt(i).toString(16):" " + response.charCodeAt(i).toString(16);
|
||||
}
|
||||
return responseHex;
|
||||
|
||||
}
|
||||
|
||||
function HexToString(response){
|
||||
var responseHex = "";
|
||||
var arr = response.split(" ");
|
||||
for(var i=0; i<arr.length; i++){
|
||||
if(arr[i] == "")
|
||||
continue;
|
||||
responseHex += String.fromCharCode(parseInt(arr[i],16));
|
||||
}
|
||||
return responseHex;
|
||||
}
|
||||
|
||||
var PAX = {
|
||||
//IP of the POS
|
||||
mDestinationIP : "http://127.0.0.1:10009", // - OLD "http://192.167.2.100:10009"; //http://112.199.49.146:8181
|
||||
|
||||
mStx : {
|
||||
hex: 0x02,
|
||||
code: "02"
|
||||
},
|
||||
|
||||
mFS : {
|
||||
hex : 0x1c,
|
||||
code : "1c"
|
||||
},
|
||||
|
||||
mEtx : {
|
||||
hex : 0x03,
|
||||
code : "03"
|
||||
},
|
||||
|
||||
mUS : {
|
||||
hex : 0x1F,
|
||||
code : "1F"
|
||||
},
|
||||
|
||||
//var _this : this;
|
||||
customData : '',
|
||||
|
||||
timeout : {
|
||||
"Initialize":120*1000,
|
||||
"GetSignature":120*1000,
|
||||
"DoSignature":120*1000,
|
||||
"DoCredit":120*1000
|
||||
},
|
||||
|
||||
//Set ip and port
|
||||
Settings : function(ip,port){
|
||||
this.mDestinationIP = "http://" + ip + ":" + port;
|
||||
console.log("New service address: "+this.mDestinationIP);
|
||||
},
|
||||
|
||||
AjaxTimeOut : function(command,timeout){
|
||||
this.timeout[command]= timeout;
|
||||
},
|
||||
|
||||
SetCustomData : function(custom_data){
|
||||
this.customData = custom_data;
|
||||
console.log(custom_data);
|
||||
},
|
||||
|
||||
//Get LRC
|
||||
getLRC : function(params){
|
||||
var lrc = 0;
|
||||
for(var i=1; i< params.length; i++){
|
||||
var type_of = typeof(params[i]);
|
||||
if(type_of == "string"){
|
||||
var element = params[i].split("");
|
||||
for(var ii=0; ii<element.length; ii++){
|
||||
lrc ^= element[ii].charCodeAt(0);
|
||||
}
|
||||
}else{
|
||||
lrc ^= params[i];
|
||||
}
|
||||
}
|
||||
return (lrc>0)?String.fromCharCode(lrc):0;
|
||||
},
|
||||
|
||||
|
||||
|
||||
//Connect to the server
|
||||
HttpCommunication : function(commandType,url,callback,timeout,fail){
|
||||
var xhr = null;
|
||||
if(window.XMLHttpRequest) {
|
||||
xhr = new XMLHttpRequest();
|
||||
} else {
|
||||
try{
|
||||
xhr = new ActiveXObject('Microsoft.XMLHttp');
|
||||
}catch(e){
|
||||
xhr = new ActiveXObject('msxml2.xmlhttp');
|
||||
}
|
||||
}
|
||||
//get
|
||||
|
||||
xhr.open("GET", url,true);
|
||||
xhr.onreadystatechange=function(){
|
||||
if(xhr.readyState==4) {
|
||||
//alert(xhr.status);
|
||||
if(xhr.status==200) {
|
||||
var response = xhr.responseText;
|
||||
console.log("Raw response: "+response);
|
||||
|
||||
var checkParams = StringToHex(response).split(" ").pop();
|
||||
var RedundancyCheck = StringToHex(response).split(" ").pop().substring(1);
|
||||
|
||||
var check = PAX.getLRC(checkParams);
|
||||
|
||||
if(check == RedundancyCheck){
|
||||
//get package detail info
|
||||
var packetInfo = [];
|
||||
var len = StringToHex(response).indexOf("03");
|
||||
var hex = StringToHex(response).slice(0,len).split(/02|1c/);
|
||||
|
||||
console.log(hex);
|
||||
if(commandType == "DoCredit"){
|
||||
var subHex=[], subPacketInfo=[];
|
||||
for(var i=0; i<hex.length; i++){
|
||||
if(hex[i] != ""){
|
||||
if(hex[i].indexOf("1f")>0){
|
||||
subHex = hex[i].split("1f");
|
||||
console.log(subHex);
|
||||
subPacketInfo = [];
|
||||
for(var j=0; j<subHex.length; j++){
|
||||
if(subHex[j]!=''){
|
||||
subPacketInfo.push(HexToString(subHex[j]));
|
||||
}
|
||||
}
|
||||
console.log(subPacketInfo);
|
||||
packetInfo.push(subPacketInfo);
|
||||
}else{
|
||||
packetInfo[i] = HexToString(hex[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}else{
|
||||
for(var i=0; i<hex.length; i++){
|
||||
if(hex[i] != ""){
|
||||
packetInfo[i] = HexToString(hex[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Separate package info: ");
|
||||
console.log(packetInfo);
|
||||
callback(packetInfo);
|
||||
}
|
||||
} else {
|
||||
if(fail) {
|
||||
fail(xhr.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
},
|
||||
|
||||
Initialize : function(initialInfo,callback,fail){
|
||||
var params = [this.mStx.hex,initialInfo.command,this.mFS.hex, initialInfo.version, this.mEtx.hex];
|
||||
//[02]A08[1c]1.28[1c]0[1c]90000[03]
|
||||
//var params = [0x02,"A08",0x1c,"1.28",0x1c, "0", 0x1c,"90000",0x03];
|
||||
var lrc = this.getLRC(params);
|
||||
var command_hex = base64ToHex($.base64.btoa(initialInfo.command));
|
||||
var version_hex = base64ToHex($.base64.btoa(initialInfo.version));
|
||||
//var elements = [this.mStx, command_hex, this.mFS, version_hex, this.mEtx, base64ToHex($.base64.btoa(lrc))];
|
||||
var elements = [this.mStx.code, command_hex, this.mFS.code, version_hex, this.mEtx.code, base64ToHex($.base64.btoa(lrc))];
|
||||
|
||||
var final_string = elements.join(" ");
|
||||
//console.log("final_string: " + final_string);
|
||||
|
||||
var final_b64 = hexToBase64(final_string);
|
||||
console.log("LRC: " + lrc);
|
||||
console.log("Base64: " + final_b64);
|
||||
var url = this.mDestinationIP + '?' + final_b64;
|
||||
console.log("URL: " + url);
|
||||
|
||||
this.HttpCommunication('Initialize',url,function(response){
|
||||
callback(response);
|
||||
},PAX.timeout.Initialize,fail);
|
||||
|
||||
},
|
||||
//GET SIGNATURE
|
||||
GetSignature : function(getSignatureInfo,callback,fail){
|
||||
|
||||
var params = [this.mStx.hex,getSignatureInfo.command,this.mFS.hex,getSignatureInfo.version,this.mFS.hex, getSignatureInfo.offset, this.mFS.hex,getSignatureInfo.requestlength,this.mEtx.hex];
|
||||
var lrc = this.getLRC(params);
|
||||
|
||||
//prepare for base64 encoding.
|
||||
var command_hex = base64ToHex($.base64.btoa(getSignatureInfo.command));
|
||||
var version_hex = base64ToHex($.base64.btoa(getSignatureInfo.version));
|
||||
var offset_hex = base64ToHex($.base64.btoa(getSignatureInfo.offset));
|
||||
var requestlength_hex = base64ToHex($.base64.btoa(getSignatureInfo.requestlength));
|
||||
//var elements = [this.mStx.code, command_hex, this.mFS.code, version_hex, this.mFS.code, offset_hex, this.mFS.code, requestlength_hex, this.mEtx.code, base64ToHex($.base64.btoa(lrc))];
|
||||
var elements = [this.mStx.code];
|
||||
elements.push(command_hex);
|
||||
elements.push(this.mFS.code);
|
||||
elements.push(version_hex);
|
||||
elements.push(this.mFS.code);
|
||||
if(offset_hex != ''){
|
||||
elements.push(offset_hex);
|
||||
}
|
||||
elements.push(this.mFS.code);
|
||||
if(requestlength_hex != ''){
|
||||
elements.push(requestlength_hex);
|
||||
}
|
||||
elements.push(this.mEtx.code);
|
||||
elements.push(base64ToHex($.base64.btoa(lrc)));
|
||||
|
||||
var final_string = elements.join(" ");
|
||||
var final_b64 = hexToBase64(final_string);
|
||||
console.log("LRC: " + lrc);
|
||||
console.log("Base64: " + final_b64);
|
||||
var url = this.mDestinationIP + '?' + final_b64;
|
||||
console.log("URL: " + url);
|
||||
|
||||
this.HttpCommunication('GetSignature',url,function(response){
|
||||
callback(response);
|
||||
},PAX.timeout.GetSignature,fail);
|
||||
|
||||
},
|
||||
|
||||
//DO SIGNATURE
|
||||
DoSignature : function(doSignatureInfo,callback,fail){
|
||||
var params = [this.mStx.hex,doSignatureInfo.command, this.mFS.hex, doSignatureInfo.version, this.mFS.hex, doSignatureInfo.uploadFlag, this.mFS.hex,doSignatureInfo.hostReferenceNumber, this.mFS.hex, doSignatureInfo.edcType, this.mFS.hex, doSignatureInfo.timeout, this.mEtx.hex];
|
||||
var lrc = this.getLRC(params);
|
||||
|
||||
//prepare for base64 encoding.
|
||||
var command_hex = base64ToHex($.base64.btoa(doSignatureInfo.command));
|
||||
var version_hex = base64ToHex($.base64.btoa(doSignatureInfo.version));
|
||||
var uploadFlag_hex = base64ToHex($.base64.btoa(doSignatureInfo.uploadFlag));
|
||||
var hostReferenceNumber_hex = base64ToHex($.base64.btoa(doSignatureInfo.hostReferenceNumber));
|
||||
var edcType_hex = base64ToHex($.base64.btoa(doSignatureInfo.edcType));
|
||||
var timeout_hex = base64ToHex($.base64.btoa(doSignatureInfo.timeout));
|
||||
var elements = [this.mStx.code];
|
||||
elements.push(command_hex);
|
||||
elements.push(this.mFS.code);
|
||||
elements.push(version_hex);
|
||||
elements.push(this.mFS.code);
|
||||
if(uploadFlag_hex != ''){
|
||||
elements.push(uploadFlag_hex);
|
||||
}
|
||||
elements.push(this.mFS.code);
|
||||
if(hostReferenceNumber_hex != ''){
|
||||
elements.push(hostReferenceNumber_hex);
|
||||
}
|
||||
elements.push(this.mFS.code);
|
||||
if(edcType_hex != ''){
|
||||
elements.push(edcType_hex);
|
||||
}
|
||||
elements.push(this.mFS.code);
|
||||
if(timeout_hex != ''){
|
||||
elements.push(timeout_hex);
|
||||
}
|
||||
elements.push(this.mEtx.code);
|
||||
elements.push(base64ToHex($.base64.btoa(lrc)));
|
||||
|
||||
var final_string = elements.join(" ");
|
||||
var final_b64 = hexToBase64(final_string);
|
||||
console.log("LRC: " + lrc);
|
||||
console.log("Base64: " + final_b64);
|
||||
var url = this.mDestinationIP + '?' + final_b64;
|
||||
console.log("URL: " + url);
|
||||
|
||||
this.HttpCommunication('DoSignature',url,function(response){
|
||||
callback(response);
|
||||
},PAX.timeout.DoSignature,fail);
|
||||
|
||||
},
|
||||
|
||||
|
||||
PushParams : function(params,type,objectInfo){
|
||||
var empty = 0;
|
||||
var arr = [];
|
||||
arr = arr.concat(params);
|
||||
for(name in objectInfo){
|
||||
if(objectInfo[name] == '' && type!="additionalInformation")
|
||||
{
|
||||
arr.push(this.mUS.hex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(type == "additionalInformation"){
|
||||
if(objectInfo[name] == ''){
|
||||
continue;
|
||||
}
|
||||
empty++;
|
||||
arr.push(name+"="+objectInfo[name].toString());
|
||||
}else{
|
||||
empty++;
|
||||
arr.push(objectInfo[name].toString());
|
||||
}
|
||||
arr.push(this.mUS.hex);
|
||||
}
|
||||
arr.pop();
|
||||
if(empty==0 && type!="additionalInformation"){
|
||||
arr = params;
|
||||
}
|
||||
if(empty==0 && type=="additionalInformation"){
|
||||
arr.push(this.mFS.hex);
|
||||
}
|
||||
//console.log(params);
|
||||
return arr;
|
||||
},
|
||||
AddBase64 : function(elements,type,objectInfo){
|
||||
//console.log(objectInfo);
|
||||
var empty = 0;
|
||||
var arr = [];
|
||||
arr = arr.concat(elements);
|
||||
for(name in objectInfo){
|
||||
if(objectInfo[name] == '' && type!="additionalInformation")
|
||||
{
|
||||
arr.push(this.mUS.code);
|
||||
continue;
|
||||
}
|
||||
if(type == "additionalInformation"){
|
||||
if(objectInfo[name] == '')
|
||||
continue;
|
||||
empty++;
|
||||
arr.push(base64ToHex($.base64.btoa(name+"="+objectInfo[name].toString())));
|
||||
}else{
|
||||
empty++;
|
||||
arr.push(base64ToHex($.base64.btoa(objectInfo[name].toString())));
|
||||
}
|
||||
arr.push(this.mUS.code);
|
||||
}
|
||||
arr.pop();
|
||||
if(empty==0 && type!="additionalInformation"){
|
||||
arr = elements;
|
||||
}
|
||||
if(empty==0 && type=="additionalInformation"){
|
||||
arr.push(this.mFS.code);
|
||||
}
|
||||
//console.log(arr);
|
||||
return arr;
|
||||
},
|
||||
//DO Credit
|
||||
DoCredit : function(doCreditInfo,callback,fail){
|
||||
var amountInformation,accountInformation,traceInformation,avsInformation,cashierInformation,commercialInformation,motoEcommerce,additionalInformation;
|
||||
var params = [this.mStx.hex,doCreditInfo.command, this.mFS.hex, doCreditInfo.version];
|
||||
params.push(this.mFS.hex);
|
||||
if(doCreditInfo.transactionType != ''){
|
||||
params.push(doCreditInfo.transactionType);
|
||||
}
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"amountInformation",doCreditInfo.amountInformation);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"accountInformation",doCreditInfo.accountInformation);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"traceInformation",doCreditInfo.traceInformation);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"avsInformation",doCreditInfo.avsInformation);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"cashierInformation",doCreditInfo.cashierInformation);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"commercialInformation",doCreditInfo.commercialInformation);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"motoEcommerce",doCreditInfo.motoEcommerce);
|
||||
|
||||
params.push(this.mFS.hex);
|
||||
params = this.PushParams(params,"additionalInformation",doCreditInfo.additionalInformation);
|
||||
|
||||
params.push(this.mEtx.hex);
|
||||
|
||||
var lrc = this.getLRC(params);
|
||||
|
||||
console.log(params);
|
||||
|
||||
//prepare for base64 encoding.
|
||||
var command_hex = base64ToHex($.base64.btoa(doCreditInfo.command));
|
||||
var version_hex = base64ToHex($.base64.btoa(doCreditInfo.version));
|
||||
var transactionType_hex = base64ToHex($.base64.btoa(doCreditInfo.transactionType));
|
||||
var amountInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.amountInformation));
|
||||
var accountInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.accountInformation));
|
||||
var traceInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.traceInformation));
|
||||
var avsInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.avsInformation));
|
||||
var cashierInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.cashierInformation));
|
||||
var commercialInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.commercialInformation));
|
||||
var motoEcommerce_hex = base64ToHex($.base64.btoa(doCreditInfo.motoEcommerce));
|
||||
var additionalInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.additionalInformation));
|
||||
|
||||
//var elements = [this.mStx.code, command_hex, this.mFS.code, version_hex, this.mFS.code, uploadFlag_hex, this.mFS.code, timeout, this.mEtx.code, base64ToHex($.base64.btoa(lrc))];
|
||||
var elements = [this.mStx.code];
|
||||
elements.push(command_hex);
|
||||
elements.push(this.mFS.code);
|
||||
elements.push(version_hex);
|
||||
elements.push(this.mFS.code);
|
||||
|
||||
if(transactionType_hex != ''){
|
||||
elements.push(transactionType_hex);
|
||||
}
|
||||
elements.push(this.mFS.code);
|
||||
|
||||
elements = this.AddBase64(elements,"amountInformation",doCreditInfo.amountInformation);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"accountInformation",doCreditInfo.accountInformation);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"traceInformation",doCreditInfo.traceInformation);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"avsInformation",doCreditInfo.avsInformation);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"cashierInformation",doCreditInfo.cashierInformation);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"commercialInformation",doCreditInfo.commercialInformation);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"motoEcommerce",doCreditInfo.motoEcommerce);
|
||||
elements.push(this.mFS.code);
|
||||
elements = this.AddBase64(elements,"additionalInformation",doCreditInfo.additionalInformation);
|
||||
|
||||
elements.push(this.mEtx.code);
|
||||
elements.push(base64ToHex($.base64.btoa(lrc)));
|
||||
console.log("elements");
|
||||
console.log(elements);
|
||||
|
||||
var final_string = elements.join(" ");
|
||||
var final_b64 = hexToBase64(final_string);
|
||||
console.log("LRC: " + lrc);
|
||||
console.log("Base64: " + final_b64);
|
||||
|
||||
// if(customData != ''){
|
||||
// final_b64 = hexToBase64(final_string+"&custom_data=<PAX>"+customData+"</PAX>");
|
||||
// }
|
||||
|
||||
|
||||
var url = this.mDestinationIP + '?' + final_b64;
|
||||
console.log("URL: " + url);
|
||||
|
||||
this.HttpCommunication('DoCredit',url,function(response){
|
||||
callback(response);
|
||||
},PAX.timeout.DoCredit,fail);
|
||||
}
|
||||
};
|
||||
|
||||
return PAX;
|
||||
|
||||
});
|
||||
349
pos_pax/static/src/js/pos_pax.js
Normal file
349
pos_pax/static/src/js/pos_pax.js
Normal file
@@ -0,0 +1,349 @@
|
||||
odoo.define('pos_pax.pos_pax', function (require) {
|
||||
"use strict";
|
||||
|
||||
// Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
var core = require('web.core');
|
||||
var screens = require('point_of_sale.screens');
|
||||
var gui = require('point_of_sale.gui');
|
||||
var pos_model = require('point_of_sale.models');
|
||||
var _t = core._t;
|
||||
var PopupWidget = require('point_of_sale.popups');
|
||||
var PaymentScreenWidget = screens.PaymentScreenWidget;
|
||||
var PAX = require('pos_pax.pax_device');
|
||||
|
||||
PAX.mDestinationIP = '';
|
||||
|
||||
pos_model.load_fields("account.journal", "pos_use_pax");
|
||||
|
||||
pos_model.PosModel = pos_model.PosModel.extend({
|
||||
|
||||
getPAXOnlinePaymentJournals: function () {
|
||||
var self = this;
|
||||
var online_payment_journals = [];
|
||||
|
||||
$.each(this.journals, function (i, val) {
|
||||
if (val.pos_use_pax) {
|
||||
online_payment_journals.push({label:self.getCashRegisterByJournalID(val.id).journal_id[1], item:val.id});
|
||||
}
|
||||
});
|
||||
return online_payment_journals;
|
||||
},
|
||||
|
||||
getCashRegisterByJournalID: function (journal_id) {
|
||||
var cashregister_return = false;
|
||||
|
||||
$.each(this.cashregisters, function (index, cashregister) {
|
||||
if (cashregister.journal_id[0] === journal_id) {
|
||||
cashregister_return = cashregister;
|
||||
}
|
||||
});
|
||||
|
||||
return cashregister_return;
|
||||
},
|
||||
|
||||
decodePAXResponse: function (data) {
|
||||
console.log(data);
|
||||
if (data[5] == 'ABORTED') {
|
||||
console.log('aborted');
|
||||
return {fail: 'Transaction Aborted'};
|
||||
} else if (data[5] == 'OK') {
|
||||
return {
|
||||
success: true,
|
||||
approval: data[6][2],
|
||||
txn_id: data[10][0],
|
||||
card_num: '***' + data[9][0],
|
||||
}
|
||||
}
|
||||
return {fail: 'Unknown Response. ' + data};
|
||||
}
|
||||
});
|
||||
|
||||
var _paylineproto = pos_model.Paymentline.prototype;
|
||||
pos_model.Paymentline = pos_model.Paymentline.extend({
|
||||
init_from_JSON: function (json) {
|
||||
_paylineproto.init_from_JSON.apply(this, arguments);
|
||||
|
||||
this.paid = true;
|
||||
this.pax_txn_pending = json.pax_txn_pending;
|
||||
this.pax_card_number = json.pax_card_number;
|
||||
this.pax_approval = json.pax_approval;
|
||||
this.pax_txn_id = json.pax_txn_id;
|
||||
|
||||
this.set_credit_card_name();
|
||||
},
|
||||
export_as_JSON: function () {
|
||||
return _.extend(_paylineproto.export_as_JSON.apply(this, arguments), {
|
||||
paid: this.paid,
|
||||
pax_txn_pending: this.pax_txn_pending,
|
||||
pax_card_number: this.pax_card_number,
|
||||
pax_approval: this.pax_approval,
|
||||
pax_txn_id: this.pax_txn_id,
|
||||
});
|
||||
},
|
||||
set_credit_card_name: function () {
|
||||
if (this.pax_card_number) {
|
||||
this.name = this.pax_card_number;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Popup to show all transaction state for the payment.
|
||||
var PAXPaymentTransactionPopupWidget = PopupWidget.extend({
|
||||
template: 'PAXPaymentTransactionPopupWidget',
|
||||
show: function (options) {
|
||||
var self = this;
|
||||
this._super(options);
|
||||
options.transaction.then(function (data) {
|
||||
if (data.auto_close) {
|
||||
setTimeout(function () {
|
||||
self.gui.close_popup();
|
||||
}, 2000);
|
||||
} else {
|
||||
self.close();
|
||||
self.$el.find('.popup').append('<div class="footer"><div class="button cancel">Ok</div></div>');
|
||||
}
|
||||
|
||||
self.$el.find('p.body').html(data.message);
|
||||
}).progress(function (data) {
|
||||
self.$el.find('p.body').html(data.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
gui.define_popup({name:'pax-payment-transaction', widget: PAXPaymentTransactionPopupWidget});
|
||||
|
||||
PaymentScreenWidget.include({
|
||||
|
||||
_get_pax_txn_pending_line: function () {
|
||||
var i = 0;
|
||||
var lines = this.pos.get_order().get_paymentlines();
|
||||
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
if (lines[i].pax_txn_pending) {
|
||||
return lines[i];
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
// Handler to manage the card reader string
|
||||
pax_credit_transaction: function (old_deferred, retry_nr) {
|
||||
var order = this.pos.get_order();
|
||||
|
||||
if(this.pos.getPAXOnlinePaymentJournals().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var transaction = {};
|
||||
var pending_line = self._get_pax_txn_pending_line();
|
||||
if ( ! pending_line ) {
|
||||
console.log('no pending line found!');
|
||||
return;
|
||||
}
|
||||
|
||||
var purchase_amount = pending_line.get_amount();
|
||||
|
||||
var transactionType = '01'; // SALE
|
||||
if (purchase_amount < 0.0) {
|
||||
purchase_amount = -purchase_amount;
|
||||
transactionType = '02'; // RETURN
|
||||
}
|
||||
|
||||
transaction = {
|
||||
command: 'T00',
|
||||
version: '1.28',
|
||||
transactionType: transactionType,
|
||||
amountInformation: {
|
||||
TransactionAmount: (purchase_amount * 100) | 0, // cast to integer
|
||||
},
|
||||
cashierInformation: {
|
||||
ClerkID: this.pos.user.name,
|
||||
},
|
||||
traceInformation: {
|
||||
ReferenceNumber: self.pos.get_order().uid,
|
||||
// InvoiceNumber: self.pos.get_order().uid,
|
||||
},
|
||||
}
|
||||
|
||||
var def = old_deferred || new $.Deferred();
|
||||
retry_nr = retry_nr || 0;
|
||||
|
||||
// show the transaction popup.
|
||||
// the transaction deferred is used to update transaction status
|
||||
// if we have a previous deferred it indicates that this is a retry
|
||||
if (! old_deferred) {
|
||||
self.gui.show_popup('pax-payment-transaction', {
|
||||
transaction: def
|
||||
});
|
||||
def.notify({
|
||||
message: _t('Handling transaction...'),
|
||||
});
|
||||
}
|
||||
|
||||
PAX.mDestinationIP = this.pos.config.pax_endpoint;
|
||||
PAX.DoCredit(transaction, function (response) {
|
||||
console.log(response);
|
||||
var parsed_response = self.pos.decodePAXResponse(response);
|
||||
if (parsed_response.fail) {
|
||||
def.resolve({message: parsed_response.fail})
|
||||
return;
|
||||
}
|
||||
if (parsed_response.success) {
|
||||
pending_line.paid = true;
|
||||
pending_line.pax_card_number = parsed_response.card_num;
|
||||
pending_line.pax_txn_id = parsed_response.txn_id;
|
||||
pending_line.pax_approval = parsed_response.approval;
|
||||
pending_line.pax_txn_pending = false;
|
||||
pending_line.set_credit_card_name();
|
||||
self.order_changes();
|
||||
self.reset_input();
|
||||
self.render_paymentlines();
|
||||
order.trigger('change', order);
|
||||
def.resolve({message: 'Approval ' + parsed_response.approval, auto_close: true})
|
||||
}
|
||||
def.resolve({message: _t('Unknown response.')})
|
||||
}, function (fail){
|
||||
def.resolve({message: _t('Communication Failure: ') + fail});
|
||||
});
|
||||
},
|
||||
|
||||
remove_paymentline_by_ref: function (line) {
|
||||
this.pos.get_order().remove_paymentline(line);
|
||||
this.reset_input();
|
||||
this.render_paymentlines();
|
||||
},
|
||||
|
||||
pax_do_reversal: function (line) {
|
||||
var def = new $.Deferred();
|
||||
var self = this;
|
||||
var transaction = {};
|
||||
|
||||
// show the transaction popup.
|
||||
// the transaction deferred is used to update transaction status
|
||||
this.gui.show_popup('pax-payment-transaction', {
|
||||
transaction: def
|
||||
});
|
||||
def.notify({
|
||||
message: 'Sending reversal...',
|
||||
});
|
||||
|
||||
transaction = {
|
||||
command: 'T00',
|
||||
version: '1.28',
|
||||
transactionType: (line.get_amount() > 0) ? '17' : '18', // V/SALE, V/RETURN
|
||||
cashierInformation: {
|
||||
ClerkID: this.pos.user.name,
|
||||
},
|
||||
traceInformation: {
|
||||
ReferenceNumber: self.pos.get_order().uid,
|
||||
InvoiceNumber: '',
|
||||
AuthCode: line.pax_approval,
|
||||
TransactionNumber: line.pax_txn_id,
|
||||
},
|
||||
}
|
||||
|
||||
PAX.mDestinationIP = this.pos.config.pax_endpoint;
|
||||
PAX.DoCredit(transaction, function (response) {
|
||||
var parsed_response = self.pos.decodePAXResponse(response);
|
||||
if (parsed_response.fail) {
|
||||
def.resolve({message: parsed_response.fail})
|
||||
return;
|
||||
}
|
||||
if (parsed_response.success) {
|
||||
def.resolve({
|
||||
message: _t('Reversal succeeded.'),
|
||||
auto_close: true,
|
||||
});
|
||||
self.remove_paymentline_by_ref(line);
|
||||
return;
|
||||
}
|
||||
def.resolve({message: _t('Unknown response.')})
|
||||
}, function (fail){
|
||||
def.resolve({message: _t('Communication Failure: ') + fail});
|
||||
});
|
||||
},
|
||||
|
||||
click_delete_paymentline: function (cid) {
|
||||
var lines = this.pos.get_order().get_paymentlines();
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].cid === cid && lines[i].pax_txn_id) {
|
||||
this.pax_do_reversal(lines[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._super(cid);
|
||||
},
|
||||
|
||||
// make sure there is only one paymentline waiting for a swipe
|
||||
click_paymentmethods: function (id) {
|
||||
var order = this.pos.get_order();
|
||||
var cashregister = null;
|
||||
for (var i = 0; i < this.pos.cashregisters.length; i++) {
|
||||
if (this.pos.cashregisters[i].journal_id[0] === id){
|
||||
cashregister = this.pos.cashregisters[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cashregister.journal.pos_use_pax) {
|
||||
var pending_line = this._get_pax_txn_pending_line();
|
||||
|
||||
if (pending_line) {
|
||||
this.gui.show_popup('error',{
|
||||
'title': _t('Error'),
|
||||
'body': _t('One credit card txn already pending.'),
|
||||
});
|
||||
} else {
|
||||
this._super(id);
|
||||
order.selected_paymentline.pax_txn_pending = true;
|
||||
this.render_paymentlines();
|
||||
order.trigger('change', order); // needed so that export_to_JSON gets triggered
|
||||
}
|
||||
} else {
|
||||
this._super(id);
|
||||
}
|
||||
},
|
||||
|
||||
click_pax_send_transaction: function (id) {
|
||||
var pending_txn_line = this._get_pax_txn_pending_line();
|
||||
if (!pending_txn_line) {
|
||||
this.gui.show_popup('error',{
|
||||
'title': _t('Error'),
|
||||
'body': _t('No pending payment line to send.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.pax_credit_transaction();
|
||||
},
|
||||
|
||||
render_paymentlines: function() {
|
||||
this._super();
|
||||
var self = this;
|
||||
self.$('.paymentlines-container').on('click', '.pax_send_transaction', function(){
|
||||
self.click_pax_send_transaction();
|
||||
});
|
||||
},
|
||||
|
||||
// before validating, get rid of any paymentlines that are pending
|
||||
validate_order: function(force_validation) {
|
||||
if (this.pos.get_order().is_paid() && ! this.invoicing) {
|
||||
var lines = this.pos.get_order().get_paymentlines();
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].pax_txn_pending) {
|
||||
this.pos.get_order().remove_paymentline(lines[i]);
|
||||
this.render_paymentlines();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._super(force_validation);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
90
pos_pax/static/src/xml/pos_pax.xml
Normal file
90
pos_pax/static/src/xml/pos_pax.xml
Normal file
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" inherit_id="point_of_sale.template">
|
||||
|
||||
<t t-name="PAXPaymentTransactionPopupWidget">
|
||||
<div class="modal-dialog">
|
||||
<div class="popup">
|
||||
<p class="title">PAX Electronic Payment</p>
|
||||
<p class="body"></p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-extend="PaymentScreen-Paymentlines">
|
||||
<t t-jquery=".col-name" t-operation="inner">
|
||||
<t t-if="line.cashregister.journal.type === 'bank'">
|
||||
<t t-if="line.pax_txn_pending">
|
||||
<div>WAITING FOR TXN</div>
|
||||
</t>
|
||||
<t t-if="! line.pax_txn_pending">
|
||||
<t t-esc='line.name' />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="btn btn-small pax_send_transaction">Send</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="line.cashregister.journal.type !== 'bank'">
|
||||
<t t-esc='line.name' />
|
||||
</t>
|
||||
</t>
|
||||
<t t-jquery="tbody tr.paymentline.selected">
|
||||
this.removeAttr('class');
|
||||
this.attr('t-attf-class', 'paymentline selected #{line.pax_txn_pending ? \'o_pos_pax_txn_pending\' : \'\'}');
|
||||
</t>
|
||||
<t t-jquery="tbody tr.paymentline[t-att-data-cid*='line.cid']">
|
||||
this.removeAttr('class');
|
||||
this.attr('t-attf-class', 'paymentline #{line.pax_txn_pending ? \'o_pos_pax_txn_pending\' : \'\'}');
|
||||
</t>
|
||||
<t t-jquery="tbody tr td.col-tendered.edit">
|
||||
this.removeAttr('class');
|
||||
this.attr('t-attf-class', 'col-tendered edit #{line.pax_txn_pending ? \'o_pos_pax_txn_pending\' : \'\'}');
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="PosPAXSignature">
|
||||
<t t-foreach="paymentlines" t-as="paymentline">
|
||||
<t t-if="!gift && paymentline.pax_txn_id && ! printed_signature">
|
||||
<br />
|
||||
<div>CARDHOLDER WILL PAY CARD ISSUER</div>
|
||||
<div>ABOVE AMOUNT PURSUANT</div>
|
||||
<div>TO CARDHOLDER AGREEMENT</div>
|
||||
<br />
|
||||
<br />
|
||||
<div>X______________________________</div>
|
||||
<t t-set="printed_signature" t-value="true"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-extend="XmlReceipt">
|
||||
<t t-jquery="t[t-foreach*='paymentlines'][t-as*='line']" t-operation="append">
|
||||
<t t-if="!gift && line.pax_auth_code">
|
||||
<line line-ratio="1">
|
||||
<left><pre> APPROVAL CODE:</pre><t t-esc="line.pax_approval"/></left>
|
||||
</line>
|
||||
</t>
|
||||
</t>
|
||||
<t t-jquery="receipt" t-operation="append">
|
||||
<div>
|
||||
<t t-call="PosPAXSignature"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-extend="PosTicket">
|
||||
<t t-jquery="t[t-foreach*='paymentlines'][t-as*='line']" t-operation="append">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<t t-if="!gift && line.pax_auth_code">
|
||||
&nbsp;&nbsp;APPROVAL CODE: <t t-esc="line.pax_approval"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-jquery="t[t-if*='receipt.footer']" t-operation="after">
|
||||
<div class="pos-center-align">
|
||||
<t t-call="PosPAXSignature"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
23
pos_pax/views/pos_config_setting_views.xml
Normal file
23
pos_pax/views/pos_config_setting_views.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="pos_config_view_form_inherit_pos_pax" model="ir.ui.view">
|
||||
<field name="name">pos.config.form.inherit.pax</field>
|
||||
<field name="model">pos.config</field>
|
||||
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='payment_methods']" position="after">
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="pos_pax">
|
||||
<div class="o_setting_right_pane">
|
||||
<strong class="o_form_label">PAX</strong>
|
||||
<div>
|
||||
<p>Your PAX Device Endpoint</p>
|
||||
<field name="pax_endpoint"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
13
pos_pax/views/pos_pax_templates.xml
Normal file
13
pos_pax/views/pos_pax_templates.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="assets" inherit_id="point_of_sale.assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/pos_pax/static/src/js/jquery_base64.js"></script>
|
||||
<script type="text/javascript" src="/pos_pax/static/src/js/pax_device.js"></script>
|
||||
<script type="text/javascript" src="/pos_pax/static/src/js/pos_pax.js"></script>
|
||||
<link rel="stylesheet" href="/pos_pax/static/src/css/pos_pax.css" />
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
40
pos_pax/views/pos_pax_views.xml
Normal file
40
pos_pax/views/pos_pax_views.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_account_journal_pos_user_form" model="ir.ui.view">
|
||||
<field name="name">POS Journal</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_account_journal_pos_user_form"></field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='amount_authorized']" position="after">
|
||||
<group attrs="{'invisible': [('type', '!=', 'bank')]}">
|
||||
<field name="pos_use_pax"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_account_bank_journal_form_inherited_pos_pax" model="ir.ui.view">
|
||||
<field name="name">account.bank.journal.form.inherited.pos.pax</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_account_bank_journal_form_inherited_pos"></field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='journal_user']" position="after">
|
||||
<field name="pos_use_pax" attrs="{'invisible': [('journal_user', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_pos_order" model="ir.ui.view">
|
||||
<field name="name">POS orders</field>
|
||||
<field name="model">pos.order</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='amount']" position="before">
|
||||
<field name="pax_card_number"/>
|
||||
<field name="pax_txn_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user