from odoo import api, fields, models, _ class HrPayslip(models.Model): """Расчётный листок + генерация проводки РСБУ при проведении. Наследует `rsbu.posting.mixin` (Этап 3): по событию проведения расчётного листка (`action_payslip_done`) идемпотентно создаётся и проводится операция в журнале РСБУ; при отмене/возврате в черновик — удаляется. Строки Дт/Кт собираются из строк расчёта по счетам, заданным на правилах (см. `hr.salary.rule`). """ _name = 'hr.payslip' _inherit = ['hr.payslip', 'rsbu.posting.mixin'] # Данные из hr.leave/календаря для автоматического расчёта (без ручных input). rsbu_norm_days = fields.Float( string='Норма рабочих дней', compute='_compute_rsbu_leave_data') rsbu_worked_days = fields.Float( string='Отработано дней', compute='_compute_rsbu_leave_data') rsbu_vacation_cal_days = fields.Float( string='Кал. дней отпуска', compute='_compute_rsbu_leave_data') rsbu_sick_cal_days = fields.Float( string='Кал. дней больничного', compute='_compute_rsbu_leave_data') rsbu_unpaid_work_days = fields.Float( string='Раб. дней без содержания', compute='_compute_rsbu_leave_data') # Дата удержания НДФЛ = дата фактической выплаты дохода. Определяет # срок перечисления налога для 6-НДФЛ (раздел 1, 6 сроков квартала). # Заполняется автоматически при проведении из даты выплаты реестра # (payslip_run_id.payment_date) с фолбэком на последний день периода; # может быть скорректирована вручную. rsbu_withholding_date = fields.Date( string='Дата удержания НДФЛ', help='Дата фактической выплаты дохода (удержания НДФЛ). Определяет ' 'срок перечисления для 6-НДФЛ. По умолчанию — дата выплаты ' 'реестра, иначе последний день расчётного периода.') def _rsbu_default_withholding_date(self): """Дата удержания по умолчанию: дата выплаты реестра или конец периода.""" self.ensure_one() run = self.payslip_run_id if run and run.payment_date: return run.payment_date return self.date_to @api.depends('employee_id', 'contract_id', 'date_from', 'date_to') def _compute_rsbu_leave_data(self): """Собрать данные об отсутствиях из hr.leave и календаря за период. Норма рабочих дней по календарю; отработанные = норма минус рабочие дни всех отсутствий; календарные дни отпуска и больничного — для расчёта отпускных и пособия. Всё берётся автоматически, без input. """ for slip in self: slip.rsbu_norm_days = 0.0 slip.rsbu_worked_days = 0.0 slip.rsbu_vacation_cal_days = 0.0 slip.rsbu_sick_cal_days = 0.0 slip.rsbu_unpaid_work_days = 0.0 contract = slip.contract_id or slip._get_employee_contracts()[:1] if not (slip.employee_id and slip.date_from and slip.date_to and contract): continue calendar = contract.resource_calendar_id if not calendar: continue # Норма рабочих дней по календарю за период (без вычета отсутствий). # _get_work_days_data_batch требует datetime (с временем), не date. from datetime import datetime, time dt_from = datetime.combine(slip.date_from, time.min) dt_to = datetime.combine(slip.date_to, time.max) data = slip.employee_id._get_work_days_data_batch( dt_from, dt_to, calendar=calendar, compute_leaves=False) norm = data.get(slip.employee_id.id, {}).get('days', 0.0) slip.rsbu_norm_days = norm # Отсутствия сотрудника за период (одобренные), по видам. leaves = self.env['hr.leave'].search([ ('employee_id', '=', slip.employee_id.id), ('state', '=', 'validate'), ('request_date_from', '<=', slip.date_to), ('request_date_to', '>=', slip.date_from), ]) work_leave_days = 0.0 # рабочие дни всех отсутствий for lv in leaves: kind = lv.holiday_status_id.rsbu_leave_kind or 'other' # рабочие дни отсутствия в пределах периода d_from = max(lv.request_date_from, slip.date_from) d_to = min(lv.request_date_to, slip.date_to) cal_days = (d_to - d_from).days + 1 wdays = lv.number_of_days # рабочие дни отсутствия (hr.leave) work_leave_days += wdays if kind == 'vacation': slip.rsbu_vacation_cal_days += cal_days elif kind == 'sick': slip.rsbu_sick_cal_days += cal_days elif kind == 'unpaid': slip.rsbu_unpaid_work_days += wdays slip.rsbu_worked_days = max(0.0, norm - work_leave_days) def action_payslip_done(self): res = super().action_payslip_done() for slip in self.filtered(lambda s: s.state == 'done'): # Проставить дату удержания НДФЛ, если не задана вручную. if not slip.rsbu_withholding_date: slip.rsbu_withholding_date = slip._rsbu_default_withholding_date() slip._rsbu_generate() return res def action_payslip_cancel(self): # Снять РСБУ-проводку до отмены расчётного листка. self._rsbu_remove() return super().action_payslip_cancel() def action_payslip_draft(self): self._rsbu_remove() return super().action_payslip_draft() # ------------------------------------------------------------------ # НДФЛ: прогрессивная шкала 13–22% нарастающим итогом + вычеты # (ст. 224, 225, 218 НК РФ, 2026). Вызывается правилом NDFL. # ------------------------------------------------------------------ # Пороги основной базы (₽, нарастающим итогом) и ставки сверх порога. # 13% до 2.4М, 15% до 5М, 18% до 20М, 20% до 50М, 22% свыше (ст.224 НК). NDFL_BRACKETS = [ (2_400_000.0, 0.13), (5_000_000.0, 0.15), (20_000_000.0, 0.18), (50_000_000.0, 0.20), (None, 0.22), ] NDFL_NONRESIDENT_RATE = 0.30 # обычный нерезидент (п.3 ст.224 НК) NDFL_CHILD_INCOME_LIMIT = 450_000.0 # лимит дохода для детских вычетов NDFL_PERSONAL_DEDUCTION = {'104': 500.0, '105': 3000.0} NDFL_GTO_DEDUCTION = 18_000.0 @api.model def _ndfl_progressive_tax(self, taxable_base): """НДФЛ по прогрессивной шкале от налоговой базы (нарастающим итогом). Повышенная ставка применяется к сумме превышения порога, не ко всему доходу (п.1 ст.225 НК). Возвращает исчисленный налог по слоям. """ if taxable_base <= 0: return 0.0 tax = 0.0 lower = 0.0 for upper, rate in self.NDFL_BRACKETS: if upper is None: tax += (taxable_base - lower) * rate break if taxable_base > upper: tax += (upper - lower) * rate lower = upper else: tax += (taxable_base - lower) * rate break return round(tax, 2) def _ndfl_prior_gross(self): """Облагаемый доход GROSS проведённых листков сотрудника за год (до текущего листка, нарастающим итогом основной базы).""" self.ensure_one() year_start = self.date_from.replace(month=1, day=1) prior = self.search([ ('employee_id', '=', self.employee_id.id), ('company_id', '=', self.company_id.id), ('state', '=', 'done'), ('date_from', '>=', year_start), ('date_from', '<', self.date_from), ('id', '!=', self.id), ]) return sum( l.total for s in prior for l in s.line_ids.filtered(lambda x: x.code == 'GROSS')) def _ndfl_payslip_months(self, include_current=True): """Число расчётных месяцев года по фактическим листкам сотрудника. Стандартные вычеты положены за месяцы налогового периода, в которых были трудовые отношения (есть расчётный листок), а не с 1 января формально. Считаем уникальные месяцы (год-месяц) проведённых листков года до текущего плюс текущий (если include_current). """ self.ensure_one() year_start = self.date_from.replace(month=1, day=1) prior = self.search([ ('employee_id', '=', self.employee_id.id), ('company_id', '=', self.company_id.id), ('state', '=', 'done'), ('date_from', '>=', year_start), ('date_from', '<', self.date_from), ('id', '!=', self.id), ]) months = {(s.date_from.year, s.date_from.month) for s in prior} if include_current: months.add((self.date_from.year, self.date_from.month)) return len(months) def _ndfl_ytd_deductions(self, ytd_total): """Стандартные вычеты сотрудника нарастающим итогом с начала года. Детские вычеты — помесячно, пока доход нарастающим итогом не превысил 450 000 ₽ (в месяц превышения не даётся); личный вычет — помесячно без лимита дохода; ГТО — разово за год. `ytd_total` — доход основной базы нарастающим итогом включая текущий месяц. """ self.ensure_one() employee = self.employee_id if not employee.rsbu_allows_deductions(): return 0.0 # Число расчётных месяцев по фактическим листкам (включая текущий). months = self._ndfl_payslip_months(include_current=True) # Детский вычет за месяц (сумма по всем детям). month_child = sum( child._deduction_amount(self.date_from) for child in employee.rsbu_child_ids) # Число месяцев года, где доход ещё не превысил лимит 450 000 ₽. # Точная помесячная проверка требует дохода каждого месяца; для # базового сценария (равномерная зарплата) оцениваем по среднему. eligible_months = months if ytd_total > self.NDFL_CHILD_INCOME_LIMIT and months > 0: avg_per_month = ytd_total / months if avg_per_month > 0: eligible_months = min(months, int( self.NDFL_CHILD_INCOME_LIMIT // avg_per_month)) child_deduction = month_child * max(0, eligible_months) # Личный вычет (500/3000 ₽) — помесячно без лимита. personal = self.NDFL_PERSONAL_DEDUCTION.get( employee.rsbu_personal_deduction_code, 0.0) * months # Вычет ГТО — разово за год. gto = self.NDFL_GTO_DEDUCTION if employee.rsbu_gto_deduction else 0.0 return round(child_deduction + personal + gto, 2) def _ndfl_tax_withheld_before(self): """НДФЛ, удержанный по предыдущим листкам года (нарастающий итог).""" self.ensure_one() year_start = self.date_from.replace(month=1, day=1) prior = self.search([ ('employee_id', '=', self.employee_id.id), ('company_id', '=', self.company_id.id), ('state', '=', 'done'), ('date_from', '>=', year_start), ('date_from', '<', self.date_from), ('id', '!=', self.id), ]) # NDFL в строках отрицательный (удержание) — берём модуль. return round(abs(sum( l.total for s in prior for l in s.line_ids.filtered(lambda x: x.code == 'NDFL'))), 2) def rsbu_compute_ndfl(self, current_gross=None): """НДФЛ текущего листка (₽, положительное число к удержанию). Алгоритм (нарастающим итогом с начала года, ст.224/225/218 НК): 1. Доход основной базы YTD = GROSS проведённых листков года + текущий GROSS; 2. Обычный нерезидент — 30% на текущий GROSS без вычетов и без нарастающего итога; иначе прогрессивная шкала 13–22% от (доход YTD − вычеты YTD); 3. Налог месяца = налог с базы YTD − налог, удержанный ранее. `current_gross` передаётся правилом из localdict (categories. RSBU_GROSS); если не задан — берётся из строк листка (после расчёта). Возвращает сумму к удержанию за текущий листок. """ self.ensure_one() employee = self.employee_id if current_gross is None: current_gross = sum(self.line_ids.filtered( lambda x: x.code == 'GROSS').mapped('total')) # Обычный нерезидент: плоские 30% на текущий доход, без вычетов. if not employee.rsbu_is_progressive_ndfl(): return round(current_gross * self.NDFL_NONRESIDENT_RATE, 2) prior_gross = self._ndfl_prior_gross() ytd_gross = prior_gross + current_gross deductions = self._ndfl_ytd_deductions(ytd_gross) taxable = max(0.0, ytd_gross - deductions) tax_ytd = self._ndfl_progressive_tax(taxable) # Налог, исчисленный с дохода прошлых месяцев (для разницы месяца). prior_deductions = self._ndfl_ytd_deductions_prior(prior_gross) prior_taxable = max(0.0, prior_gross - prior_deductions) tax_prior = self._ndfl_progressive_tax(prior_taxable) tax_month = round(tax_ytd - tax_prior, 2) return max(0.0, tax_month) def _ndfl_ytd_deductions_prior(self, prior_gross): """Вычеты, накопленные к ПРЕДЫДУЩЕМУ месяцу (для разницы налога). Аналог _ndfl_ytd_deductions, но за месяцы года ДО текущего. """ self.ensure_one() employee = self.employee_id if not employee.rsbu_allows_deductions(): return 0.0 months_prior = self._ndfl_payslip_months(include_current=False) if months_prior <= 0: return 0.0 month_child = sum( child._deduction_amount(self.date_from) for child in employee.rsbu_child_ids) eligible_months = months_prior if prior_gross > self.NDFL_CHILD_INCOME_LIMIT and months_prior > 0: avg = prior_gross / months_prior if avg > 0: eligible_months = min(months_prior, int( self.NDFL_CHILD_INCOME_LIMIT // avg)) child_deduction = month_child * max(0, eligible_months) personal = self.NDFL_PERSONAL_DEDUCTION.get( employee.rsbu_personal_deduction_code, 0.0) * months_prior gto = self.NDFL_GTO_DEDUCTION if employee.rsbu_gto_deduction else 0.0 return round(child_deduction + personal + gto, 2) # ------------------------------------------------------------------ # Показатели 6-НДФЛ по листку (для агрегации rsbu.ndfl.summary) # ------------------------------------------------------------------ def _ndfl_report_values(self): """Показатели листка для 6-НДФЛ: начислено, вычеты, база, исчислено, удержано, ставка (верхняя применённая), дата удержания. Начислено (GROSS) и удержанный НДФЛ — из строк листка. Вычеты за месяц — разница YTD-вычетов. База = начислено − вычеты (не ниже 0). Исчислено для резидента ≈ удержано месяца. Ставка — верхняя ступень прогрессивной шкалы, в которую попал доход YTD (для группировки РасчСумНал по ставке). """ self.ensure_one() gross = round(sum(self.line_ids.filtered( lambda x: x.code == 'GROSS').mapped('total')), 2) withheld = round(abs(sum(self.line_ids.filtered( lambda x: x.code == 'NDFL').mapped('total'))), 2) employee = self.employee_id # Вычет месяца = YTD-вычеты (вкл. текущий) − YTD-вычеты до текущего. if employee.rsbu_allows_deductions(): prior_gross = self._ndfl_prior_gross() ded_ytd = self._ndfl_ytd_deductions(prior_gross + gross) ded_prior = self._ndfl_ytd_deductions_prior(prior_gross) deduction = max(0.0, round(ded_ytd - ded_prior, 2)) else: deduction = 0.0 base = max(0.0, round(gross - deduction, 2)) rate = self._ndfl_report_rate() return { 'gross': gross, 'deduction': deduction, 'base': base, 'computed': withheld, 'withheld': withheld, 'rate': rate, 'withholding_date': ( self.rsbu_withholding_date or self._rsbu_default_withholding_date()), } def _ndfl_report_rate(self): """Верхняя ставка прогрессивной шкалы по доходу YTD (в процентах). Для нерезидента — 30%. Для резидента — ставка той ступени, в которую попадает совокупный доход года (нарастающим итогом). """ self.ensure_one() if not self.employee_id.rsbu_is_progressive_ndfl(): return int(round(self.NDFL_NONRESIDENT_RATE * 100)) gross = round(sum(self.line_ids.filtered( lambda x: x.code == 'GROSS').mapped('total')), 2) ytd = self._ndfl_prior_gross() + gross lower = 0.0 rate = self.NDFL_BRACKETS[0][1] for upper, r in self.NDFL_BRACKETS: rate = r if upper is None or ytd <= upper: break lower = upper return int(round(rate * 100)) # ------------------------------------------------------------------ # Разбивка страховых взносов по видам для РСВ (ОПС/ОМС/ВНиМ) # ------------------------------------------------------------------ def _rsbu_insurance_gross(self): """Начисленные выплаты (GROSS) текущего листка — для РСВ «Выплаты».""" self.ensure_one() by_code = {l.code: l.total for l in self.line_ids} gross = sum(self.line_ids.filtered( lambda x: x.category_id.code == 'RSBU_GROSS').mapped('total')) if not gross: gross = by_code.get('GROSS', 0.0) return round(gross, 2) def rsbu_insurance_base(self): """База страховых взносов текущего листка (GROSS − необлагаемое). Необлагаемая часть — больничный (SICK3, не облагается взносами). Аналогично правилу INSURANCE (base = RSBU_GROSS − SICK3). Читается из строк листка; если строки ещё не сохранены (момент расчёта), предпочтительно передавать base явно в rsbu_insurance_breakdown. """ self.ensure_one() by_code = {l.code: l.total for l in self.line_ids} gross = sum(self.line_ids.filtered( lambda x: x.category_id.code == 'RSBU_GROSS').mapped('total')) if not gross: gross = by_code.get('GROSS', 0.0) sick = by_code.get('SICK3', 0.0) return max(0.0, gross - sick) def _rsbu_insurance_ytd_base(self): """База страховых взносов сотрудника с начала года ДО текущего листка. Нужна для регрессии: годовая предельная база ОПС считается нарастающим итогом. Суммирует базу (GROSS − SICK3) по проведённым листкам года, предшествующим текущему. """ self.ensure_one() year_start = self.date_from.replace(month=1, day=1) prior = self.search([ ('employee_id', '=', self.employee_id.id), ('company_id', '=', self.company_id.id), ('state', '=', 'done'), ('date_from', '>=', year_start), ('date_from', '<', self.date_from), ('id', '!=', self.id), ]) total = 0.0 for slip in prior: total += slip.rsbu_insurance_base() return round(total, 2) def rsbu_insurance_breakdown(self, base=None): """Разбивка взносов текущего листка по видам для РСВ: dict. Возвращает {'ops', 'oms', 'vnim', 'total', 'base', 'base_regular', 'base_msp', 'base_within_limit', 'base_over_limit', 'ops_within', 'ops_over'}. Базовый тариф применяется к части базы в пределах 1.5 МРОТ (при льготе МСП) или ко всей базе; часть сверх 1.5 МРОТ — по льготному совокупному тарифу МСП (ОПС/ОМС пропорционально, ВНиМ по льготе = 0). Регрессия ОПС: годовая предельная база (rsbu_insurance_base_limit) считается нарастающим итогом с начала года. База текущего листка делится на часть в пределах лимита (base_within_limit) и сверх (base_over_limit); взносы ОПС на превышение начисляются по пониженному тарифу (для единого пониженного МСП сверх лимита — тот же льготный тариф). ОМС предела не имеет. """ self.ensure_one() company = self.company_id if base is None: base = self.rsbu_insurance_base() empty = {'ops': 0.0, 'oms': 0.0, 'vnim': 0.0, 'total': 0.0, 'base': 0.0, 'base_regular': 0.0, 'base_msp': 0.0, 'base_within_limit': 0.0, 'base_over_limit': 0.0, 'ops_within': 0.0, 'ops_over': 0.0} if base <= 0: return empty r_ops = (company.rsbu_rate_ops or 0.0) / 100.0 r_oms = (company.rsbu_rate_oms or 0.0) / 100.0 r_vnim = (company.rsbu_rate_vnim or 0.0) / 100.0 # --- Разбивка базы по годовой предельной базе (регрессия ОПС) --- limit = company.rsbu_insurance_base_limit or 0.0 if limit > 0: ytd = self._rsbu_insurance_ytd_base() remaining = max(0.0, limit - ytd) # ещё в пределах лимита base_within_limit = min(base, remaining) base_over_limit = max(0.0, base - base_within_limit) else: base_within_limit = base base_over_limit = 0.0 # --- Разбивка части В ПРЕДЕЛАХ лимита по порогу 1.5 МРОТ (МСП) --- base_regular = base base_msp = 0.0 if company.rsbu_msp_enabled and company.rsbu_min_wage: threshold = 1.5 * company.rsbu_min_wage base_regular = min(base, threshold) base_msp = max(0.0, base - threshold) # ОМС/ВНиМ считаем по базе в пределах 1.5 МРОТ (обычный тариф) плюс # льготная часть МСП (ОМС — да, ВНиМ по льготе = 0). Предел базы # к ОМС не применяется. oms = base_regular * r_oms vnim = base_regular * r_vnim # ОПС в пределах лимита: часть base_within_limit по обычному тарифу # (в пределах 1.5 МРОТ) + льготному (сверх 1.5 МРОТ). # Для простоты и корректности начисления считаем ОПС отдельно по # обычной и льготной частям всей базы, затем разносим на within/over. ops_regular = base_regular * r_ops ops_msp = 0.0 if base_msp: r_msp = (company.rsbu_msp_rate or 0.0) / 100.0 msp_total = base_msp * r_msp share = (r_ops + r_oms) or 1.0 ops_msp = msp_total * (r_ops / share) oms += msp_total * (r_oms / share) ops_total = ops_regular + ops_msp # Разнесение ОПС на within/over по доле базы в пределах лимита. if base > 0: frac_within = base_within_limit / base else: frac_within = 1.0 ops_within = round(ops_total * frac_within, 2) ops_over = round(ops_total - ops_within, 2) return { 'ops': round(ops_total, 2), 'oms': round(oms, 2), 'vnim': round(vnim, 2), 'total': round(ops_total + oms + vnim, 2), 'base': round(base, 2), 'base_regular': round(base_regular, 2), 'base_msp': round(base_msp, 2), 'base_within_limit': round(base_within_limit, 2), 'base_over_limit': round(base_over_limit, 2), 'ops_within': ops_within, 'ops_over': ops_over, } # ------------------------------------------------------------------ # Учёт отсутствий из hr.leave: раздельные коды по виду отсутствия # ------------------------------------------------------------------ # Код строки worked_days по виду отсутствия (rsbu_leave_kind): RSBU_LEAVE_CODE = { 'vacation': 'LV_VACATION', 'sick': 'LV_SICK', 'unpaid': 'LV_UNPAID', 'other': 'LV_OTHER', } def _compute_leave_days(self, contract, day_from, day_to): """Строки отсутствий с раздельными кодами по виду (РСБУ). Штатный payroll сваливает все отсутствия в код GLOBAL с отрицательным числом дней. Нам нужно различать отпуск/больничный/ без содержания, чтобы правила расчёта тянули данные автоматически. Формируем по одной строке worked_days на КАЖДЫЙ вид отсутствия (rsbu_leave_kind типа отсутствия), с ПОЛОЖИТЕЛЬНЫМ числом дней/часов — так формулам удобнее (дни отсутствия каждого вида). """ self.ensure_one() from pytz import timezone from datetime import datetime, time leaves = {} calendar = contract.resource_calendar_id tz = timezone(calendar.tz or contract.employee_id.tz or 'UTC') day_leave_intervals = contract.employee_id.list_leaves( day_from, day_to, calendar=calendar) for day, hours, leave in day_leave_intervals: holiday = leave[:1].holiday_id leave_type = holiday.holiday_status_id kind = leave_type.rsbu_leave_kind or 'other' code = self.RSBU_LEAVE_CODE.get(kind, 'LV_OTHER') struct = leaves.setdefault(code, { 'name': leave_type.name or _('Отсутствие'), 'sequence': 20, 'code': code, 'number_of_days': 0.0, 'number_of_hours': 0.0, 'contract_id': contract.id, }) struct['number_of_hours'] += hours work_hours = calendar.get_work_hours_count( tz.localize(datetime.combine(day, time.min)), tz.localize(datetime.combine(day, time.max)), compute_leaves=False) if work_hours: struct['number_of_days'] += hours / work_hours return list(leaves.values()) # ------------------------------------------------------------------ # Точки расширения rsbu.posting.mixin # ------------------------------------------------------------------ def _rsbu_is_applicable(self): """Проводку формируем для проведённого расчётного листка.""" self.ensure_one() return self.state == 'done' def _rsbu_move_date(self): """Дата проводки начисления — конец расчётного периода.""" self.ensure_one() return self.date_to or fields.Date.context_today(self) def _rsbu_get_move_values(self): vals = super()._rsbu_get_move_values() # Начисление зарплаты в КУДиР (кассовый метод) не участвует — # признание идёт при выплате/уплате. Помечаем, чтобы пересчёт # КУДиР не подхватывал начисление. vals['usn_recognition'] = 'none' return vals def _rsbu_build_lines(self): """Собрать строки rsbu.move.line из строк расчёта. По каждому правилу с суммой и заданными счетами РСБУ формируем две строки (дебет и кредит) на модуль суммы. Отрицательные суммы (удержания) и режим сторно (`credit_note`) инвертируют стороны. Субконто «сотрудник» проставляется, где включено на правиле (обязательно для счёта 70). """ self.ensure_one() lines = [] employee = self.employee_id for pline in self.line_ids: rule = pline.salary_rule_id debit_account = rule.rsbu_account_debit credit_account = rule.rsbu_account_credit # Правило без счетов РСБУ не порождает проводку (напр. GROSS/NET — # промежуточные итоги, счетов не имеют). if not debit_account and not credit_account: continue # Направление проводки задаётся счетами правила (Дт/Кт), а сумма — # модуль строки расчёта. Знак total лишь отражает роль правила в # расчёте NET (удержания заданы «минусом»), но не направление # проводки. Сторно (credit_note) инвертирует стороны. amount = abs(pline.total or 0.0) if not amount: continue invert = bool(self.credit_note) base_subconto = {} if rule.rsbu_subconto_employee and employee: base_subconto['employee_id'] = employee.id if debit_account: lines.append(self._rsbu_payroll_line( debit_account, pline.name, debit=0.0 if invert else amount, credit=amount if invert else 0.0, subconto=base_subconto)) if credit_account: lines.append(self._rsbu_payroll_line( credit_account, pline.name, debit=amount if invert else 0.0, credit=0.0 if invert else amount, subconto=base_subconto)) return lines def _rsbu_payroll_line(self, account, name, debit=0.0, credit=0.0, subconto=None): """Хелпер значений строки rsbu.move.line. Субконто «сотрудник» на счёте требуется планом счетов только для 70; проставляем его на всех строках с включённым флагом (безопасно — для прочих счетов это просто аналитика). """ vals = { 'account_id': account.id, 'name': name or _('Начисление зарплаты'), 'debit': debit, 'credit': credit, } if subconto: vals.update(subconto) # Счёт может обязательно требовать субконто «сотрудник» (напр. 70): # если правило не проставило employee, но счёт требует — подставим. if 'employee_id' not in vals and account.subconto_employee and self.employee_id: vals['employee_id'] = self.employee_id.id return vals