diff --git a/l10n_ru_act_rev/README.md b/l10n_ru_act_rev/README.md new file mode 100644 index 0000000..101fe21 --- /dev/null +++ b/l10n_ru_act_rev/README.md @@ -0,0 +1,14 @@ +# Российская локализация - Акт сверки +name: l10n_ru_act_rev + + +## Описание +Добавление печатной формы акт сверки из контактов, с помощью которой можно легко отслеживать дебеторские и кредиторские проводки с клиентами. + +### Для печати: +1. Выбираем меню Контакты - карточку конкретного партнера - Действия - "Печать акт сверки"; +2. В визарде выбираем: + 2.1. Компанию (для которой нужна сверка с выбранным контактом); + 2.2. Период сверки; + 2.3. Цель (один из режимом: все проведенные проводки или все проводки, включая черновики); +3. Кнопка "Печать" \ No newline at end of file diff --git a/l10n_ru_act_rev/__init__.py b/l10n_ru_act_rev/__init__.py new file mode 100644 index 0000000..778f647 --- /dev/null +++ b/l10n_ru_act_rev/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import report +from . import wizard +from . import controllers diff --git a/l10n_ru_act_rev/__manifest__.py b/l10n_ru_act_rev/__manifest__.py new file mode 100644 index 0000000..840b9e8 --- /dev/null +++ b/l10n_ru_act_rev/__manifest__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Российская локализация - Акт сверки", + + 'summary': """ + Добавление отчета акт сверки""", + + 'description': """ + Добавление формы акт сверки, находящегося в контактах, с помощью которого можно легко отслеживать дебеторские и кредиторские проводки с клиентами. + + Для печати: + 1. Выбираем меню Контакты - конкретного партнера - Действия - "Печать акт сверки"; + 2. В визарде выбираем: + 2.1. Компанию (для которой нужна сверка с выбранным контактом); + 2.2. Период сверки; + 2.3. Цель (один из режимом: все проведенные проводки или все проводки, включая черновики); + 3. Кнопка "Печать" + + """, + + 'author': "MK.Lab", + 'website': "https://www.inf-centre.ru/", + + 'category': 'Uncategorized', + 'version': '0.1', + + # any module necessary for this one to work correctly + "depends": ["account", "portal", "website", 'contacts', "l10n_ru_doc", 'l10n_ru_contract', 'l10n_ru_base'], + "data": [ + "security/ir.model.access.csv", + "wizard/general_ledger_wizard_view.xml", + "report/layouts.xml", + "report/general_ledger.xml", + "views/account_account_views.xml", + "views/report_general_ledger.xml", + "views/portal_templates.xml", + ], + "installable": True, + "application": True, + "auto_install": False, + # only loaded in demonstration mode + 'demo': [ + 'demo/demo.xml', + ], +} diff --git a/l10n_ru_act_rev/controllers/__init__.py b/l10n_ru_act_rev/controllers/__init__.py new file mode 100644 index 0000000..457bae2 --- /dev/null +++ b/l10n_ru_act_rev/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/l10n_ru_act_rev/controllers/controllers.py b/l10n_ru_act_rev/controllers/controllers.py new file mode 100644 index 0000000..86e717e --- /dev/null +++ b/l10n_ru_act_rev/controllers/controllers.py @@ -0,0 +1,89 @@ +from odoo import http +from odoo.http import request +from datetime import datetime, date + + +class ActRevise(http.Controller): + + @http.route(['/my/act_revise/'], type='http', auth="public", website=True) + def print_report(self): + partner = request.env.user.partner_id.parent_id.id + partner_name = request.env.user.partner_id.parent_id.name + if not partner: + partner = request.env.user.partner_id.id + partner_name = request.env.user.partner_id.name + company = request.env.user.company_id.id + company_name = request.env.user.company_id.name + today = date.today() + d1 = today.strftime("%d.%m.%y") + + wizard_data = { + "target_move": "posted", + "hide_account_at_0": True, + "foreign_currency": True, + "company_id": company, + "partner_ids": [partner], + "show_cost_center": True, + "centralize": True + } + wizard_record = request.env['general.ledger.act_revise.wizard'].sudo().create(wizard_data) + + action = request.env.ref('l10n_ru_act_rev.action_general_ledger_wizard').read()[0] + action['res_id'] = wizard_record.id + action['context'] = dict(request.env.context) + return request.redirect('/web#action=' + str(action['id']) + '&id=' + str(wizard_record.id) + '&view_type=form') + + # @http.route(['/my/act_revise/'], type='http', auth="public", website=True) + # def print_report(self): + # partner = request.env.user.partner_id.parent_id.id + # partner_name = request.env.user.partner_id.parent_id.name + # if not partner: + # partner = request.env.user.partner_id.id + # partner_name = request.env.user.partner_id.name + # company = request.env.user.company_id.id + # company_name = request.env.user.company_id.name + # today = date.today() + # d1 = today.strftime("%d.%m.%y") + # # new_url = str('Акт Сверки ' + d1 + ' ' + company_name + '_' + partner_name) + # # new_url=str('AC ' + company +' - ' + partner + ' ' + d1) + # # if request.httprequest.full_path == '/my/act_revise/a?': + # # return werkzeug.utils.redirect('/my/act_revise/%s' % new_url) + # wizard_data = {"target_move": "posted", + # "hide_account_at_0": True, + # "foreign_currency": True, + # #"show_analytic_tags": True, + # "company_id": company, + # "partner_ids": [partner], + # #"show_partner_details": True, + # "show_cost_center": True, + # "centralize": True} + # t = request.env['general.ledger.act_revise.wizard'].sudo().create(wizard_data) + # data = t._prepare_report_general_ledger() + # name = t.get_report_filename() + # report_name = name.encode('cp1251') + # pdf, _ = request.env['ir.actions.report']._render_qweb_pdf( + # 'act_revise.action_print_report_general_ledger_qweb', res_ids=t.id, data=data) + # pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf)), ] + # return request.make_response(pdf, headers=pdfhttpheaders) + + @http.route(['/my/act_revise_contact/'], type='http', auth="public", website=True) + def print_report_contact(self, date_to, date_from, target_move, company, partner): + partner_id = int(partner) or 'default_partner_value' + company_id = int(company) + wizard_data = {"date_to": date_to, + "date_from": date_from, + "target_move": target_move, + "hide_account_at_0": True, + "foreign_currency": True, + #"show_analytic_tags": True, + "company_id": company_id, + "partner_ids": [partner_id], + #"show_partner_details": True, + "show_cost_center": True, + "centralize": True} + t = request.env['general.ledger.act_revise.wizard'].sudo().create(wizard_data) + data = t._prepare_report_general_ledger() + pdf, _ = request.env['ir.actions.report']._render_qweb_pdf( + 'l10n_ru_act_rev.action_print_report_general_ledger_qweb', res_ids=t.id, data=data) + pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf)), ] + return request.make_response(pdf, headers=pdfhttpheaders) \ No newline at end of file diff --git a/l10n_ru_act_rev/demo/demo.xml b/l10n_ru_act_rev/demo/demo.xml new file mode 100644 index 0000000..340fdd0 --- /dev/null +++ b/l10n_ru_act_rev/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/l10n_ru_act_rev/models/__init__.py b/l10n_ru_act_rev/models/__init__.py new file mode 100644 index 0000000..f75a803 --- /dev/null +++ b/l10n_ru_act_rev/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_account +from . import ir_actions_report \ No newline at end of file diff --git a/l10n_ru_act_rev/models/account_account.py b/l10n_ru_act_rev/models/account_account.py new file mode 100644 index 0000000..3e251fe --- /dev/null +++ b/l10n_ru_act_rev/models/account_account.py @@ -0,0 +1,7 @@ +from odoo import fields, models, _ + + +class AccountAccount(models.Model): + _inherit = "account.account" + + centralized = fields.Boolean(_("Централизованно")) diff --git a/l10n_ru_act_rev/models/ir_actions_report.py b/l10n_ru_act_rev/models/ir_actions_report.py new file mode 100644 index 0000000..b0f8377 --- /dev/null +++ b/l10n_ru_act_rev/models/ir_actions_report.py @@ -0,0 +1,10 @@ +from odoo import api, models + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + @api.model + def _prepare_account_financial_report_context(self, data): + lang = data and data.get("account_financial_report_lang") or "" + return dict(self.env.context or {}, lang=lang) if lang else False diff --git a/l10n_ru_act_rev/report/__init__.py b/l10n_ru_act_rev/report/__init__.py new file mode 100644 index 0000000..f7ccab0 --- /dev/null +++ b/l10n_ru_act_rev/report/__init__.py @@ -0,0 +1,2 @@ +from . import general_ledger + diff --git a/l10n_ru_act_rev/report/general_ledger.py b/l10n_ru_act_rev/report/general_ledger.py new file mode 100644 index 0000000..d2c99d8 --- /dev/null +++ b/l10n_ru_act_rev/report/general_ledger.py @@ -0,0 +1,1045 @@ +import calendar +import datetime +import operator + +from odoo.exceptions import UserError +from odoo import _, api, models +from odoo.tools import float_is_zero + +class GeneralLedgerReport(models.AbstractModel): + _name = "report.act_revise.general_ledger" + _description = "General Ledger Report" + + COMMON_ML_FIELDS = [ + "account_id", + "partner_id", + "journal_id", + "move_type", + "date", + "ref", + "id", + "move_id", + "name", + ] + + @api.model + def _get_move_lines_domain_not_reconciled( + self, company_id, account_ids, partner_ids, only_posted_moves, date_from + ): + domain = [ + ("account_id", "in", account_ids), + ("company_id", "=", company_id), + ("reconciled", "=", False), + ] + if partner_ids: + domain += [("partner_id", "in", partner_ids)] + if only_posted_moves: + domain += [("move_id.state", "=", "posted")] + else: + domain += [("move_id.state", "in", ["posted", "draft"])] + if date_from: + domain += [("date", ">", date_from)] + return domain + + @api.model + def _get_new_move_lines_domain( + self, new_ml_ids, account_ids, company_id, partner_ids, only_posted_moves + ): + domain = [ + ("account_id", "in", account_ids), + ("company_id", "=", company_id), + ("id", "in", new_ml_ids), + ] + if partner_ids: + domain += [("partner_id", "in", partner_ids)] + if only_posted_moves: + domain += [("move_id.state", "=", "posted")] + else: + domain += [("move_id.state", "in", ["posted", "draft"])] + return domain + + def _recalculate_move_lines( + self, + move_lines, + move_type, + debit_ids, + credit_ids, + debit_amount, + credit_amount, + ml_ids, + account_ids, + company_id, + partner_ids, + only_posted_moves, + debit_amount_currency, + credit_amount_currency, + ): + debit_ids = set(debit_ids) + credit_ids = set(credit_ids) + in_credit_but_not_in_debit = credit_ids - debit_ids + reconciled_ids = list(debit_ids) + list(in_credit_but_not_in_debit) + reconciled_ids = set(reconciled_ids) + ml_ids = set(ml_ids) + new_ml_ids = reconciled_ids - ml_ids + new_ml_ids = list(new_ml_ids) + new_domain = self._get_new_move_lines_domain( + new_ml_ids, account_ids, company_id, partner_ids, only_posted_moves + ) + company_currency = self.env["res.company"].browse(company_id).currency_id + ml_fields = self._get_ml_fields() + new_move_lines = self.env["account.move.line"].search_read( + domain=new_domain, fields=ml_fields + ) + move_lines = move_lines + new_move_lines + for move_line in move_lines: + ml_id = move_line["id"] + if ml_id in debit_ids: + if move_line.get("amount_residual", False): + move_line["amount_residual"] += debit_amount[ml_id] + else: + move_line["amount_residual"] = debit_amount[ml_id] + if move_line.get("amount_residual_currency", False): + move_line["amount_residual_currency"] += debit_amount_currency[ + ml_id + ] + else: + move_line["amount_residual_currency"] = debit_amount_currency[ml_id] + if ml_id in credit_ids: + if move_line.get("amount_residual", False): + move_line["amount_residual"] -= credit_amount[ml_id] + else: + move_line["amount_residual"] = -credit_amount[ml_id] + if move_line.get("amount_residual_currency", False): + move_line["amount_residual_currency"] -= credit_amount_currency[ + ml_id + ] + else: + move_line["amount_residual_currency"] = -credit_amount_currency[ + ml_id + ] + # Set amount_currency=0 to keep the same behaviour as in v13 + # Conditions: if there is no curency_id defined or it is equal + # to the company's curency_id + if "amount_currency" in move_line and ( + "currency_id" not in move_line + or move_line["currency_id"] == company_currency.id + ): + move_line["amount_currency"] = 0 + return move_lines + + def _get_accounts_data(self, accounts_ids): + accounts = self.env["account.account"].browse(accounts_ids) + accounts_data = {} + for account in accounts: + accounts_data.update( + { + account.id: { + "id": account.id, + "code": account.code, + "name": account.name, + "hide_account": False, + "group_id": account.group_id.id, + "currency_id": account.currency_id.id, + "currency_name": account.currency_id.name, + "centralized": account.centralized, + } + } + ) + return accounts_data + + def _get_journals_data(self, journals_ids): + journals = self.env["account.journal"].browse(journals_ids) + journals_data = {} + for journal in journals: + journals_data.update({journal.id: {"id": journal.id, "code": journal.code}}) + return journals_data + + def _get_analytic_data(self, account_ids): + analytic_accounts = self.env["account.analytic.account"].browse(account_ids) + analytic_data = {} + for account in analytic_accounts: + analytic_data.update({account.id: {"name": account.name}}) + return analytic_data + + def _get_taxes_data(self, taxes_ids): + taxes = self.env["account.tax"].browse(taxes_ids) + taxes_data = {} + for tax in taxes: + taxes_data.update( + { + tax.id: { + "id": tax.id, + "amount": tax.amount, + "amount_type": tax.amount_type, + "display_name": tax.display_name, + } + } + ) + if tax.amount_type == "percent" or tax.amount_type == "division": + taxes_data[tax.id]["string"] = "%" + else: + taxes_data[tax.id]["string"] = "" + taxes_data[tax.id]["tax_name"] = ( + tax.display_name + + " (" + + str(tax.amount) + + taxes_data[tax.id]["string"] + + ")" + ) + return taxes_data + + def _get_account_type_domain(self, grouped_by): + """To avoid set all possible types, set in or not in as operator of the types + we are interested in. In v15 we used the internal_type field (type of + account.account.type).""" + at_op = "in" if grouped_by != "taxes" else "not in" + return [ + ("account_type", at_op, ["asset_receivable", "liability_payable"]), + ] + + def _get_acc_prt_accounts_ids(self, company_id, grouped_by): + accounts_domain = [ + ("company_id", "=", company_id), + ] + self._get_account_type_domain(grouped_by) + acc_prt_accounts = self.env["account.account"].search(accounts_domain) + return acc_prt_accounts.ids + + def _get_initial_balances_bs_ml_domain( + self, account_ids, company_id, date_from, base_domain, grouped_by, acc_prt=False + ): + accounts_domain = [ + ("company_id", "=", company_id), + ("include_initial_balance", "=", True), + ] + if account_ids: + accounts_domain += [("id", "in", account_ids)] + domain = [] + domain += base_domain + domain += [("date", "<", date_from)] + accounts = self.env["account.account"].search(accounts_domain) + domain += [("account_id", "in", accounts.ids)] + if acc_prt: + domain += self._get_account_type_domain(grouped_by) + return domain + + def _get_initial_balances_pl_ml_domain( + self, account_ids, company_id, date_from, fy_start_date, base_domain + ): + accounts_domain = [ + ("company_id", "=", company_id), + ("include_initial_balance", "=", False), + ] + if account_ids: + accounts_domain += [("id", "in", account_ids)] + domain = [] + domain += base_domain + domain += [("date", "<", date_from), ("date", ">=", fy_start_date)] + accounts = self.env["account.account"].search(accounts_domain) + domain += [("account_id", "in", accounts.ids)] + return domain + + def _get_accounts_initial_balance(self, initial_domain_bs, initial_domain_pl): + gl_initial_acc_bs = self.env["account.move.line"].read_group( + domain=initial_domain_bs, + fields=["account_id", "debit", "credit", "balance", "amount_currency:sum"], + groupby=["account_id"], + ) + gl_initial_acc_pl = self.env["account.move.line"].read_group( + domain=initial_domain_pl, + fields=["account_id", "debit", "credit", "balance", "amount_currency:sum"], + groupby=["account_id"], + ) + gl_initial_acc = gl_initial_acc_bs + gl_initial_acc_pl + return gl_initial_acc + + def _get_initial_balance_fy_pl_ml_domain( + self, account_ids, company_id, fy_start_date, base_domain + ): + accounts_domain = [ + ("company_id", "=", company_id), + ("include_initial_balance", "=", False), + ] + if account_ids: + accounts_domain += [("id", "in", account_ids)] + domain = [] + domain += base_domain + domain += [("date", "<", fy_start_date)] + accounts = self.env["account.account"].search(accounts_domain) + domain += [("account_id", "in", accounts.ids)] + return domain + + def _get_pl_initial_balance( + self, account_ids, company_id, fy_start_date, foreign_currency, base_domain + ): + domain = self._get_initial_balance_fy_pl_ml_domain( + account_ids, company_id, fy_start_date, base_domain + ) + initial_balances = self.env["account.move.line"].read_group( + domain=domain, + fields=["account_id", "debit", "credit", "balance", "amount_currency:sum"], + groupby=["account_id"], + ) + pl_initial_balance = { + "debit": 0.0, + "credit": 0.0, + "balance": 0.0, + "bal_curr": 0.0, + } + for initial_balance in initial_balances: + pl_initial_balance["debit"] += initial_balance["debit"] + pl_initial_balance["credit"] += initial_balance["credit"] + pl_initial_balance["balance"] += initial_balance["balance"] + pl_initial_balance["bal_curr"] += initial_balance["amount_currency"] + return pl_initial_balance + + def _get_gl_initial_acc( + self, account_ids, company_id, date_from, fy_start_date, base_domain, grouped_by + ): + initial_domain_bs = self._get_initial_balances_bs_ml_domain( + account_ids, company_id, date_from, base_domain, grouped_by + ) + initial_domain_pl = self._get_initial_balances_pl_ml_domain( + account_ids, company_id, date_from, fy_start_date, base_domain + ) + return self._get_accounts_initial_balance(initial_domain_bs, initial_domain_pl) + + def _prepare_gen_ld_data_item(self, gl): + res = {} + for key_bal in ["init_bal", "fin_bal"]: + res[key_bal] = {} + for key_field in ["credit", "debit", "balance", "bal_curr"]: + field_name = key_field if key_field != "bal_curr" else "amount_currency" + res[key_bal][key_field] = gl[field_name] + return res + + def _prepare_gen_ld_data(self, gl_initial_acc, domain, grouped_by): + data = {} + for gl in gl_initial_acc: + acc_id = gl["account_id"][0] + data[acc_id] = self._prepare_gen_ld_data_item(gl) + data[acc_id]["id"] = acc_id + if grouped_by: + data[acc_id][grouped_by] = False + method = "_prepare_gen_ld_data_group_%s" % grouped_by + if not hasattr(self, method): + return data + return getattr(self, method)(data, domain, grouped_by) + + def _prepare_gen_ld_data_group_partners(self, data, domain, grouped_by): + gl_initial_acc_prt = self.env["account.move.line"].read_group( + domain=domain, + fields=[ + "account_id", + "partner_id", + "debit", + "credit", + "balance", + "amount_currency:sum", + ], + groupby=["account_id", "partner_id"], + lazy=False, + ) + if gl_initial_acc_prt: + for gl in gl_initial_acc_prt: + if not gl["partner_id"]: + prt_id = 0 + prt_name = _("Missing Partner") + else: + prt_id = gl["partner_id"][0] + prt_name = gl["partner_id"][1] + prt_name = prt_name._value + acc_id = gl["account_id"][0] + data[acc_id][prt_id] = self._prepare_gen_ld_data_item(gl) + data[acc_id][prt_id]["id"] = prt_id + data[acc_id][prt_id]["name"] = prt_name + data[acc_id][grouped_by] = True + return data + + def _prepare_gen_ld_data_group_taxes(self, data, domain, grouped_by): + gl_initial_acc_prt = self.env["account.move.line"].read_group( + domain=domain, + fields=[ + "account_id", + "debit", + "credit", + "balance", + "amount_currency:sum", + "tax_line_id", + ], + groupby=["account_id"], + lazy=False, + ) + if gl_initial_acc_prt: + for gl in gl_initial_acc_prt: + if "tax_line_id" in gl and gl["tax_line_id"]: + tax_id = gl["tax_line_id"][0] + tax_name = gl["tax_line_id"][1] + tax_name = tax_name._value + else: + tax_id = 0 + tax_name = "Missing Tax" + acc_id = gl["account_id"][0] + data[acc_id][tax_id] = self._prepare_gen_ld_data_item(gl) + data[acc_id][tax_id]["id"] = tax_id + data[acc_id][tax_id]["name"] = tax_name + data[acc_id][grouped_by] = True + return data + + def _get_initial_balance_data( + self, + account_ids, + partner_ids, + company_id, + date_from, + foreign_currency, + only_posted_moves, + unaffected_earnings_account, + fy_start_date, + cost_center_ids, + extra_domain, + grouped_by, + ): + # If explicit list of accounts is provided, + # don't include unaffected earnings account + if account_ids: + unaffected_earnings_account = False + base_domain = [] + if company_id: + base_domain += [("company_id", "=", company_id)] + if partner_ids: + base_domain += [("partner_id", "in", partner_ids)] + if only_posted_moves: + base_domain += [("move_id.state", "=", "posted")] + else: + base_domain += [("move_id.state", "in", ["posted", "draft"])] + if cost_center_ids: + base_domain += [("analytic_account_ids", "in", cost_center_ids)] + if extra_domain: + base_domain += extra_domain + gl_initial_acc = self._get_gl_initial_acc( + account_ids, company_id, date_from, fy_start_date, base_domain, grouped_by + ) + domain = self._get_initial_balances_bs_ml_domain( + account_ids, company_id, date_from, base_domain, grouped_by, acc_prt=True + ) + data = self._prepare_gen_ld_data(gl_initial_acc, domain, grouped_by) + accounts_ids = list(data.keys()) + unaffected_id = unaffected_earnings_account + if unaffected_id: + if unaffected_id not in accounts_ids: + accounts_ids.append(unaffected_id) + data[unaffected_id] = self._initialize_data(foreign_currency) + data[unaffected_id]["id"] = unaffected_id + data[unaffected_id]["mame"] = "" + data[unaffected_id][grouped_by] = False + pl_initial_balance = self._get_pl_initial_balance( + account_ids, company_id, fy_start_date, foreign_currency, base_domain + ) + for key_bal in ["init_bal", "fin_bal"]: + fields_balance = ["credit", "debit", "balance"] + if foreign_currency: + fields_balance.append("bal_curr") + for field_name in fields_balance: + data[unaffected_id][key_bal][field_name] += pl_initial_balance[ + field_name + ] + return data + + @api.model + def _get_move_line_data(self, move_line): + move_type = move_line.get("move_type", "") + inscription = "" + account = self.env['account.account'].browse(move_line['account_id'][0]) + accounttype = account.account_type + if move_type == "entry": + if accounttype == "liability_payable": + inscription = "Платеж поставщику" + elif accounttype == "asset_receivable": + inscription = "Оплата покупателя" + # elif accounttype == "asset_current": + # inscription = "Оплата покупателя" + # else: + # inscription = "" + elif move_type == "out_invoice": + inscription = "Продажа товаров и услуг" + # elif move_type == "out_refund": + # inscription = "Сторно клиента " + elif move_type == "in_invoice": + inscription = "Покупка товаров и услуг " + # elif move_type == "in_refund": + # inscription = "Кредитное обязательство поставщика " + # elif move_type == "out_receipt": + # inscription = "Квитанция продаж" + # elif move_type == "in_receipt": + # inscription = "Квитанция покупки" + + transformed_move_name = f"{inscription} No. {move_line['move_name']}" + move_line_data = { + "id": move_line["id"], + "date": move_line["date"], + "entry": transformed_move_name, + # "entry": move_line["move_name"], + "entry_id": move_line["move_id"][0], + "journal_id": move_line["journal_id"][0], + "account_id": move_line["account_id"][0], + "partner_id": move_line["partner_id"][0] + if move_line["partner_id"] + else False, + "partner_name": move_line["partner_id"][1] + if move_line["partner_id"] + else "", + "ref": "" if not move_line["ref"] else move_line["ref"], + "name": "" if not move_line["name"] else move_line["name"], + "tax_ids": move_line["tax_ids"], + "tax_line_id": move_line["tax_line_id"], + "debit": move_line["debit"], + "credit": move_line["credit"], + "balance": move_line["balance"], + "bal_curr": move_line["amount_currency"], + "rec_id": move_line["full_reconcile_id"][0] + if move_line["full_reconcile_id"] + else False, + "rec_name": move_line["full_reconcile_id"][1] + if move_line["full_reconcile_id"] + else "", + "currency_id": move_line["currency_id"], + "analytic_distribution": move_line["analytic_distribution"] or {}, + } + if ( + move_line_data["ref"] == move_line_data["name"] + or move_line_data["ref"] == "" + ): + ref_label = move_line_data["name"] + elif move_line_data["name"] == "": + ref_label = move_line_data["ref"] + else: + ref_label = move_line_data["ref"] + str(" - ") + move_line_data["name"] + move_line_data.update({"ref_label": ref_label}) + return move_line_data + + @api.model + def _get_period_domain( + self, + account_ids, + partner_ids, + company_id, + only_posted_moves, + date_to, + date_from, + cost_center_ids, + ): + domain = [ + ("display_type", "not in", ["line_note", "line_section"]), + ("date", ">=", date_from), + ("date", "<=", date_to), + ] + if account_ids: + domain += [("account_id", "in", account_ids)] + if company_id: + domain += [("company_id", "=", company_id)] + if partner_ids: + domain += [("partner_id", "in", partner_ids)] + if only_posted_moves: + domain += [("move_id.state", "=", "posted")] + else: + domain += [("move_id.state", "in", ["posted", "draft"])] + + if cost_center_ids: + domain += [("analytic_account_ids", "in", cost_center_ids)] + return domain + + def _initialize_data(self, foreign_currency): + res = {} + for key_bal in ["init_bal", "fin_bal"]: + res[key_bal] = {} + for key_field in ["balance", "credit", "debit"]: + res[key_bal][key_field] = 0.0 + if foreign_currency: + res[key_bal]["bal_curr"] = 0.0 + return res + + def _get_reconciled_after_date_to_ids(self, full_reconcile_ids, date_to): + full_reconcile_ids = list(full_reconcile_ids) + domain = [ + ("max_date", ">", date_to), + ("full_reconcile_id", "in", full_reconcile_ids), + ] + fields = ["full_reconcile_id"] + reconciled_after_date_to = self.env["account.partial.reconcile"].search_read( + domain=domain, fields=fields + ) + rec_after_date_to_ids = list( + map(operator.itemgetter("full_reconcile_id"), reconciled_after_date_to) + ) + rec_after_date_to_ids = [i[0] for i in rec_after_date_to_ids] + return rec_after_date_to_ids + + def _prepare_ml_items(self, move_line, grouped_by): + res = [] + if grouped_by == "partners": + item_id = move_line["partner_id"][0] if move_line["partner_id"] else 0 + item_name = ( + move_line["partner_id"][1] + if move_line["partner_id"] + else _("Missing Partner") + ) + res.append({"id": item_id, "name": item_name}) + elif grouped_by == "taxes": + if move_line["tax_line_id"]: + item_id = move_line["tax_line_id"][0] + item_name = move_line["tax_line_id"][1] + res.append({"id": item_id, "name": item_name}) + elif move_line["tax_ids"]: + for tax_id in move_line["tax_ids"]: + tax_item = self.env["account.tax"].browse(tax_id) + res.append({"id": tax_item.id, "name": tax_item.name}) + else: + res.append({"id": 0, "name": "Missing Tax"}) + else: + res.append({"id": 0, "name": ""}) + return res + + def _get_period_ml_data( + self, + account_ids, + partner_ids, + company_id, + foreign_currency, + only_posted_moves, + date_from, + date_to, + gen_ld_data, + cost_center_ids, + extra_domain, + grouped_by, + ): + domain = self._get_period_domain( + account_ids, + partner_ids, + company_id, + only_posted_moves, + date_to, + date_from, + cost_center_ids, + ) + if extra_domain: + domain += extra_domain + ml_fields = self._get_ml_fields() + move_lines = self.env["account.move.line"].search_read( + domain=domain, fields=ml_fields, order="date,move_name" + ) + journal_ids = set() + full_reconcile_ids = set() + taxes_ids = set() + analytic_ids = set() + full_reconcile_data = {} + acc_prt_account_ids = self._get_acc_prt_accounts_ids(company_id, grouped_by) + for move_line in move_lines: + journal_ids.add(move_line["journal_id"][0]) + for tax_id in move_line["tax_ids"]: + taxes_ids.add(tax_id) + for analytic_account in move_line["analytic_distribution"] or {}: + analytic_ids.add(int(analytic_account)) + if move_line["full_reconcile_id"]: + rec_id = move_line["full_reconcile_id"][0] + if rec_id not in full_reconcile_ids: + full_reconcile_data.update( + { + rec_id: { + "id": rec_id, + "name": move_line["full_reconcile_id"][1], + } + } + ) + full_reconcile_ids.add(rec_id) + acc_id = move_line["account_id"][0] + ml_id = move_line["id"] + if acc_id not in gen_ld_data.keys(): + gen_ld_data[acc_id] = self._initialize_data(foreign_currency) + gen_ld_data[acc_id]["id"] = acc_id + gen_ld_data[acc_id]["mame"] = move_line["account_id"][1] + if grouped_by: + gen_ld_data[acc_id][grouped_by] = False + if acc_id in acc_prt_account_ids: + item_ids = self._prepare_ml_items(move_line, grouped_by) + for item in item_ids: + item_id = item["id"] + if item_id not in gen_ld_data[acc_id]: + if grouped_by: + gen_ld_data[acc_id][grouped_by] = True + gen_ld_data[acc_id][item_id] = self._initialize_data( + foreign_currency + ) + gen_ld_data[acc_id][item_id]["id"] = item_id + gen_ld_data[acc_id][item_id]["name"] = item["name"] + gen_ld_data[acc_id][item_id][ml_id] = self._get_move_line_data( + move_line + ) + gen_ld_data[acc_id][item_id]["fin_bal"]["credit"] += move_line[ + "credit" + ] + gen_ld_data[acc_id][item_id]["fin_bal"]["debit"] += move_line[ + "debit" + ] + gen_ld_data[acc_id][item_id]["fin_bal"]["balance"] += move_line[ + "balance" + ] + if foreign_currency: + gen_ld_data[acc_id][item_id]["fin_bal"][ + "bal_curr" + ] += move_line["amount_currency"] + else: + gen_ld_data[acc_id][ml_id] = self._get_move_line_data(move_line) + gen_ld_data[acc_id]["fin_bal"]["credit"] += move_line["credit"] + gen_ld_data[acc_id]["fin_bal"]["debit"] += move_line["debit"] + gen_ld_data[acc_id]["fin_bal"]["balance"] += move_line["balance"] + if foreign_currency: + gen_ld_data[acc_id]["fin_bal"]["bal_curr"] += move_line[ + "amount_currency" + ] + journals_data = self._get_journals_data(list(journal_ids)) + accounts_data = self._get_accounts_data(gen_ld_data.keys()) + taxes_data = self._get_taxes_data(list(taxes_ids)) + analytic_data = self._get_analytic_data(list(analytic_ids)) + rec_after_date_to_ids = self._get_reconciled_after_date_to_ids( + full_reconcile_data.keys(), date_to + ) + return ( + gen_ld_data, + accounts_data, + journals_data, + full_reconcile_data, + taxes_data, + analytic_data, + rec_after_date_to_ids, + ) + + @api.model + def _recalculate_cumul_balance( + self, move_lines, last_cumul_balance, rec_after_date_to_ids + ): + for move_line in move_lines: + move_line["balance"] += last_cumul_balance + last_cumul_balance = move_line["balance"] + if move_line["rec_id"] in rec_after_date_to_ids: + move_line["rec_name"] = "(" + _("future") + ") " + move_line["rec_name"] + return move_lines + + def _create_account(self, account, acc_id, gen_led_data, rec_after_date_to_ids): + move_lines = [] + for ml_id in gen_led_data[acc_id].keys(): + if not isinstance(ml_id, int): + account.update({ml_id: gen_led_data[acc_id][ml_id]}) + else: + move_lines += [gen_led_data[acc_id][ml_id]] + move_lines = sorted(move_lines, key=lambda k: (k["date"])) + move_lines = self._recalculate_cumul_balance( + move_lines, + gen_led_data[acc_id]["init_bal"]["balance"], + rec_after_date_to_ids, + ) + account.update({"move_lines": move_lines}) + return account + + def _create_account_not_show_item( + self, account, acc_id, gen_led_data, rec_after_date_to_ids, grouped_by + ): + move_lines = [] + for prt_id in gen_led_data[acc_id].keys(): + if not isinstance(prt_id, int): + account.update({prt_id: gen_led_data[acc_id][prt_id]}) + elif isinstance(gen_led_data[acc_id][prt_id], dict): + for ml_id in gen_led_data[acc_id][prt_id].keys(): + if isinstance(ml_id, int): + move_lines += [gen_led_data[acc_id][prt_id][ml_id]] + move_lines = sorted(move_lines, key=lambda k: (k["date"])) + move_lines = self._recalculate_cumul_balance( + move_lines, + gen_led_data[acc_id]["init_bal"]["balance"], + rec_after_date_to_ids, + ) + account.update({"move_lines": move_lines, grouped_by: False}) + return account + + def _get_list_grouped_item( + self, data, account, rec_after_date_to_ids, hide_account_at_0, rounding + ): + list_grouped = [] + for data_id in data.keys(): + group_item = {} + move_lines = [] + if not isinstance(data_id, int): + account.update({data_id: data[data_id]}) + else: + for ml_id in data[data_id].keys(): + if not isinstance(ml_id, int): + group_item.update({ml_id: data[data_id][ml_id]}) + else: + move_lines += [data[data_id][ml_id]] + move_lines = sorted(move_lines, key=lambda k: (k["date"])) + move_lines = self._recalculate_cumul_balance( + move_lines, + data[data_id]["init_bal"]["balance"], + rec_after_date_to_ids, + ) + group_item.update({"move_lines": move_lines}) + if ( + hide_account_at_0 + and float_is_zero( + data[data_id]["init_bal"]["balance"], + precision_rounding=rounding, + ) + and group_item["move_lines"] == [] + ): + continue + list_grouped += [group_item] + return account, list_grouped + + def _create_general_ledger( + self, + gen_led_data, + accounts_data, + grouped_by, + rec_after_date_to_ids, + hide_account_at_0, + ): + general_ledger = [] + rounding = self.env.company.currency_id.rounding + for acc_id in gen_led_data.keys(): + account = {} + account.update( + { + "code": accounts_data[acc_id]["code"], + "name": accounts_data[acc_id]["name"], + "type": "account", + "currency_id": accounts_data[acc_id]["currency_id"], + "centralized": accounts_data[acc_id]["centralized"], + "grouped_by": grouped_by, + } + ) + if grouped_by and not gen_led_data[acc_id][grouped_by]: + account = self._create_account( + account, acc_id, gen_led_data, rec_after_date_to_ids + ) + if ( + hide_account_at_0 + and float_is_zero( + gen_led_data[acc_id]["init_bal"]["balance"], + precision_rounding=rounding, + ) + and account["move_lines"] == [] + ): + continue + else: + if grouped_by: + account, list_grouped = self._get_list_grouped_item( + gen_led_data[acc_id], + account, + rec_after_date_to_ids, + hide_account_at_0, + rounding, + ) + account.update({"list_grouped": list_grouped}) + if ( + hide_account_at_0 + and float_is_zero( + gen_led_data[acc_id]["init_bal"]["balance"], + precision_rounding=rounding, + ) + and account["list_grouped"] == [] + ): + continue + else: + account = self._create_account_not_show_item( + account, acc_id, gen_led_data, rec_after_date_to_ids, grouped_by + ) + if ( + hide_account_at_0 + and float_is_zero( + gen_led_data[acc_id]["init_bal"]["balance"], + precision_rounding=rounding, + ) + and account["move_lines"] == [] + ): + continue + general_ledger += [account] + return general_ledger + + @api.model + def _calculate_centralization(self, centralized_ml, move_line, date_to): + jnl_id = move_line["journal_id"] + month = move_line["date"].month + if jnl_id not in centralized_ml.keys(): + centralized_ml[jnl_id] = {} + if month not in centralized_ml[jnl_id].keys(): + centralized_ml[jnl_id][month] = {} + last_day_month = calendar.monthrange(move_line["date"].year, month) + date = datetime.date(move_line["date"].year, month, last_day_month[1]) + if date > date_to: + date = date_to + centralized_ml[jnl_id][month].update( + { + "journal_id": jnl_id, + "ref_label": "Centralized entries", + "date": date, + "debit": 0.0, + "credit": 0.0, + "balance": 0.0, + "bal_curr": 0.0, + "partner_id": False, + "rec_id": 0, + "entry_id": False, + "tax_ids": [], + "tax_line_id": False, + "full_reconcile_id": False, + "id": False, + "currency_id": False, + "analytic_distribution": {}, + } + ) + centralized_ml[jnl_id][month]["debit"] += move_line["debit"] + centralized_ml[jnl_id][month]["credit"] += move_line["credit"] + centralized_ml[jnl_id][month]["balance"] += ( + move_line["debit"] - move_line["credit"] + ) + centralized_ml[jnl_id][month]["bal_curr"] += move_line["bal_curr"] + return centralized_ml + + @api.model + def _get_centralized_ml(self, account, date_to, grouped_by): + centralized_ml = {} + if isinstance(date_to, str): + date_to = datetime.datetime.strptime(date_to, "%Y-%m-%d").date() + if grouped_by and account[grouped_by]: + for item in account["list_grouped"]: + for move_line in item["move_lines"]: + centralized_ml = self._calculate_centralization( + centralized_ml, + move_line, + date_to, + ) + else: + for move_line in account["move_lines"]: + centralized_ml = self._calculate_centralization( + centralized_ml, + move_line, + date_to, + ) + list_centralized_ml = [] + for jnl_id in centralized_ml.keys(): + list_centralized_ml += list(centralized_ml[jnl_id].values()) + return list_centralized_ml + + def _get_report_values(self, docids, data): + wizard_id = data["wizard_id"] + company = self.env["res.company"].browse(data["company_id"]) + company_id = data["company_id"] + date_to = data["date_to"] + date_from = data["date_from"] + partner_ids = data["partner_ids"] + account_ids = data["account_ids"] + cost_center_ids = data["cost_center_ids"] + grouped_by = data["grouped_by"] + hide_account_at_0 = data["hide_account_at_0"] + foreign_currency = data["foreign_currency"] + only_posted_moves = data["only_posted_moves"] + unaffected_earnings_account = data["unaffected_earnings_account"] + fy_start_date = data["fy_start_date"] + extra_domain = data["domain"] + gen_ld_data = self._get_initial_balance_data( + account_ids, + partner_ids, + company_id, + date_from, + foreign_currency, + only_posted_moves, + unaffected_earnings_account, + fy_start_date, + cost_center_ids, + extra_domain, + grouped_by, + ) + centralize = data["centralize"] + ( + gen_ld_data, + accounts_data, + journals_data, + full_reconcile_data, + taxes_data, + analytic_data, + rec_after_date_to_ids, + ) = self._get_period_ml_data( + account_ids, + partner_ids, + company_id, + foreign_currency, + only_posted_moves, + date_from, + date_to, + gen_ld_data, + cost_center_ids, + extra_domain, + grouped_by, + ) + general_ledger = self._create_general_ledger( + gen_ld_data, + accounts_data, + grouped_by, + rec_after_date_to_ids, + hide_account_at_0, + ) + if centralize: + for account in general_ledger: + if account["centralized"]: + centralized_ml = self._get_centralized_ml( + account, date_to, grouped_by + ) + account["move_lines"] = centralized_ml + account["move_lines"] = self._recalculate_cumul_balance( + account["move_lines"], + gen_ld_data[account["id"]]["init_bal"]["balance"], + rec_after_date_to_ids, + ) + if grouped_by and account[grouped_by]: + account[grouped_by] = False + del account["list_grouped"] + general_ledger = sorted(general_ledger, key=lambda k: k["code"]) + if not general_ledger: + raise UserError(f'Проводок для формирования акта по введенным условиям не найдено.') + return { + "doc_ids": [wizard_id], + "doc_model": "general.ledger.act_revise.wizard", + "docs": self.env["general.ledger.act_revise.wizard"].browse(wizard_id), + "foreign_currency": data["foreign_currency"], + "company_name": company.display_name, + "company_currency": company.currency_id, + "currency_name": company.currency_id.name, + "date_from": data["date_from"], + "date_to": data["date_to"], + "only_posted_moves": data["only_posted_moves"], + "hide_account_at_0": data["hide_account_at_0"], + "show_cost_center": data["show_cost_center"], + "general_ledger": general_ledger, + "accounts_data": accounts_data, + "journals_data": journals_data, + "full_reconcile_data": full_reconcile_data, + "taxes_data": taxes_data, + "centralize": centralize, + "analytic_data": analytic_data, + "filter_partner_ids": True if partner_ids else False, + "currency_model": self.env["res.currency"], + } + + def _get_ml_fields(self): + return self.COMMON_ML_FIELDS + [ + "analytic_distribution", + "full_reconcile_id", + "tax_line_id", + "currency_id", + "credit", + "debit", + "amount_currency", + "balance", + "tax_ids", + "move_name", + ] diff --git a/l10n_ru_act_rev/report/general_ledger.xml b/l10n_ru_act_rev/report/general_ledger.xml new file mode 100644 index 0000000..b7add8b --- /dev/null +++ b/l10n_ru_act_rev/report/general_ledger.xml @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + Акт сверки + general.ledger.act_revise.wizard + qweb-pdf + l10n_ru_act_rev.general_ledger + l10n_ru_act_rev.general_ledger + 'Акт сверки - %s' % (object.get_report_filename() or '') + + + + + Account financial report qweb paperformat + + custom + 297 + 210 + Landscape + 12 + 8 + 5 + 5 + + 10 + 110 + + + + + \ No newline at end of file diff --git a/l10n_ru_act_rev/report/layouts.xml b/l10n_ru_act_rev/report/layouts.xml new file mode 100644 index 0000000..e77d9c7 --- /dev/null +++ b/l10n_ru_act_rev/report/layouts.xml @@ -0,0 +1,34 @@ + + + + + diff --git a/l10n_ru_act_rev/security/ir.model.access.csv b/l10n_ru_act_rev/security/ir.model.access.csv new file mode 100644 index 0000000..96c0232 --- /dev/null +++ b/l10n_ru_act_rev/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_general_ledger_act_revise_wizard,general.ledger.act_revise.wizard,model_general_ledger_act_revise_wizard,base.group_user,1,1,1,1 diff --git a/l10n_ru_act_rev/views/account_account_views.xml b/l10n_ru_act_rev/views/account_account_views.xml new file mode 100644 index 0000000..38dce94 --- /dev/null +++ b/l10n_ru_act_rev/views/account_account_views.xml @@ -0,0 +1,14 @@ + + + + account.account.form.inherit + + account.account + form + + + + + + + diff --git a/l10n_ru_act_rev/views/portal_templates.xml b/l10n_ru_act_rev/views/portal_templates.xml new file mode 100644 index 0000000..b7a1ff2 --- /dev/null +++ b/l10n_ru_act_rev/views/portal_templates.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/l10n_ru_act_rev/views/report_general_ledger.xml b/l10n_ru_act_rev/views/report_general_ledger.xml new file mode 100644 index 0000000..9e6945e --- /dev/null +++ b/l10n_ru_act_rev/views/report_general_ledger.xml @@ -0,0 +1,9 @@ + + + + diff --git a/l10n_ru_act_rev/wizard/__init__.py b/l10n_ru_act_rev/wizard/__init__.py new file mode 100644 index 0000000..c0d813c --- /dev/null +++ b/l10n_ru_act_rev/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import abstract_wizard +from . import general_ledger_wizard diff --git a/l10n_ru_act_rev/wizard/abstract_wizard.py b/l10n_ru_act_rev/wizard/abstract_wizard.py new file mode 100644 index 0000000..216bae7 --- /dev/null +++ b/l10n_ru_act_rev/wizard/abstract_wizard.py @@ -0,0 +1,38 @@ +from odoo import models, fields + + +class AbstractWizard(models.AbstractModel): + _name = "act_revise.abstract_wizard" + _description = "Abstract Wizard" + + def _get_partner_ids_domain(self): + return [ + "&", + "|", + ("company_id", "=", self.company_id.id), + ("company_id", "=", False), + "|", + ("parent_id", "=", False), + ("is_company", "=", True), + ] + + def _default_partners(self): + context = self.env.context + if context.get("active_ids") and context.get("active_model") == "res.partner": + partners = self.env["res.partner"].browse(context["active_ids"]) + corp_partners = partners.filtered("parent_id") + partners -= corp_partners + partners |= corp_partners.mapped("commercial_partner_id") + return partners.ids + + company_id = fields.Many2one( + comodel_name="res.company", + default=lambda self: self.env.company.id, + required=False, + string="Компания", + ) + + def button_export_pdf(self): + self.ensure_one() + report_type = "qweb-pdf" + return self._export(report_type) \ No newline at end of file diff --git a/l10n_ru_act_rev/wizard/general_ledger_wizard.py b/l10n_ru_act_rev/wizard/general_ledger_wizard.py new file mode 100644 index 0000000..d0d08e0 --- /dev/null +++ b/l10n_ru_act_rev/wizard/general_ledger_wizard.py @@ -0,0 +1,898 @@ +import logging +import time +from ast import literal_eval +from odoo import _, api, fields, models +from odoo.tools import date_utils,pycompat +from pytils import dt,numeral +from datetime import datetime, date +import re +import urllib +from odoo.exceptions import UserError + +class GeneralLedgerReportWizard(models.TransientModel): + """General ledger report wizard.""" + + _name = "general.ledger.act_revise.wizard" + _description = "General Ledger Report Wizard" + _inherit = "act_revise.abstract_wizard" + + # date_range_id = fields.Many2one(comodel_name="date.range", string="Date range") + date_from = fields.Date(string="Начало даты", required=True, default=lambda self: self._init_date_from()) + date_to = fields.Date(string="Конец даты", required=True, default=fields.Date.context_today) + fy_start_date = fields.Date(compute="_compute_fy_start_date") + target_move = fields.Selection( + [("posted", "Все проведенные проводки"), ("all", "Все проводки")], + string="Цель операции", + required=True, + default="posted", + ) + account_ids = fields.Many2many( + comodel_name="account.account", string=_("Filter accounts") + ) + centralize = fields.Boolean(string=_("Activate centralization"), default=True) + hide_account_at_0 = fields.Boolean( + string=_("Hide account ending balance at 0"), + help=_("Use this filter to hide an account or a partner " + "with an ending balance at 0. " + "If partners are filtered, " + "debits and credits totals will not match the trial balance."), + ) + receivable_accounts_only = fields.Boolean() + payable_accounts_only = fields.Boolean() + partner_ids = fields.Many2many( + comodel_name="res.partner", + string=_("Filter partners"), + default=lambda self: self._default_partners(), + ) + account_journal_ids = fields.Many2many( + comodel_name="account.journal", string=_("Filter journals") + ) + cost_center_ids = fields.Many2many( + comodel_name="account.analytic.account", string=_("Filter cost centers") + ) + + not_only_one_unaffected_earnings_account = fields.Boolean(readonly=True) + foreign_currency = fields.Boolean( + string=_("Show foreign currency"), + help=_("Display foreign currency for move lines, unless " + "account currency is not setup through chart of accounts " + "will display initial and final balance in that currency."), + default=lambda self: self._default_foreign_currency(), + ) + account_code_from = fields.Many2one( + comodel_name="account.account", + help="Starting account in a range", + ) + account_code_to = fields.Many2one( + comodel_name="account.account", + help="Ending account in a range", + ) + grouped_by = fields.Selection( + selection=[("", "None"), ("partners", "Partners"), ("taxes", "Taxes")], + default="partners", + ) + show_cost_center = fields.Boolean( + string="Show Analytic Account", + default=True, + ) + domain = fields.Char( + string="Journal Items Domain", + default=[], + help="This domain will be used to select specific domain for Journal " "Items", + ) + + # def _print_report(self, report_type): + # self.ensure_one() + # data = self._prepare_report_general_ledger() + # report = self.env["ir.actions.report"].search( + # [("report_name", "=", "act_revise.general_ledger"), ("report_type", "=", report_type)], limit=1, ) + # if self.partner_ids[0].parent_id: + # partner = int(self.partner_ids[0].parent_id.id) + # else: + # partner = int(self.partner_ids[0].id) + # return { + # 'type': 'ir.actions.act_url', + # 'url': '/my/act_revise_contact/%s?date_to=%s&date_from=%s&target_move=%s&company=%s&partner=%s' % ( + # urllib.parse.quote(self.get_report_filename()), self.date_to, self.date_from, self.target_move, + # self.company_id.id, partner), + # 'target': 'new', + # } + + def _get_account_move_lines_domain(self): + domain = literal_eval(self.domain) if self.domain else [] + return domain + + @api.onchange("account_code_from", "account_code_to") + def on_change_account_range(self): + if ( + self.account_code_from + and self.account_code_from.code.isdigit() + and self.account_code_to + and self.account_code_to.code.isdigit() + ): + start_range = self.account_code_from.code + end_range = self.account_code_to.code + self.account_ids = self.env["account.account"].search( + [("code", ">=", start_range), ("code", "<=", end_range)] + ) + if self.company_id: + self.account_ids = self.account_ids.filtered( + lambda a: a.company_id == self.company_id + ) + + def _init_date_from(self): + """set start date to begin of current year if fiscal year running""" + today = fields.Date.context_today(self) + company = self.company_id or self.env.company + last_fsc_month = company.fiscalyear_last_month + last_fsc_day = company.fiscalyear_last_day + + if ( + today.month < int(last_fsc_month) + or today.month == int(last_fsc_month) + and today.day <= last_fsc_day + ): + return time.strftime("%Y-01-01") + else: + return False + + def _default_foreign_currency(self): + return self.env.user.has_group("base.group_multi_currency") + + @api.depends("date_from") + def _compute_fy_start_date(self): + for wiz in self: + if wiz.date_from: + date_from, date_to = date_utils.get_fiscal_year( + wiz.date_from, + day=self.company_id.fiscalyear_last_day, + month=int(self.company_id.fiscalyear_last_month), + ) + wiz.fy_start_date = date_from + else: + wiz.fy_start_date = False + + @api.onchange("company_id") + def onchange_company_id(self): + """Handle company change.""" + count = self.env["account.account"].search_count( + [ + ("account_type", "=", "equity_unaffected"), + ("company_id", "=", self.company_id.id), + ] + ) + self.not_only_one_unaffected_earnings_account = count != 1 + # if ( + # self.company_id + # and self.date_range_id.company_id + # and self.date_range_id.company_id != self.company_id + # ): + # self.date_range_id = False + if self.company_id and self.account_journal_ids: + self.account_journal_ids = self.account_journal_ids.filtered( + lambda p: p.company_id == self.company_id or not p.company_id + ) + if self.company_id and self.partner_ids: + self.partner_ids = self.partner_ids.filtered( + lambda p: p.company_id == self.company_id or not p.company_id + ) + if self.company_id and self.account_ids: + if self.receivable_accounts_only or self.payable_accounts_only: + self.onchange_type_accounts_only() + else: + self.account_ids = self.account_ids.filtered( + lambda a: a.company_id == self.company_id + ) + if self.company_id and self.cost_center_ids: + self.cost_center_ids = self.cost_center_ids.filtered( + lambda c: c.company_id == self.company_id + ) + res = { + "domain": { + "account_ids": [], + "partner_ids": [], + "account_journal_ids": [], + "cost_center_ids": [], + # "date_range_id": [], + } + } + if not self.company_id: + return res + else: + res["domain"]["account_ids"] += [("company_id", "=", self.company_id.id)] + res["domain"]["account_journal_ids"] += [ + ("company_id", "=", self.company_id.id) + ] + res["domain"]["partner_ids"] += self._get_partner_ids_domain() + res["domain"]["cost_center_ids"] += [ + ("company_id", "=", self.company_id.id) + ] + # res["domain"]["date_range_id"] += [ + # "|", + # ("company_id", "=", self.company_id.id), + # ("company_id", "=", False), + # ] + return res + + # @api.onchange("date_range_id") + # def onchange_date_range_id(self): + # """Handle date range change.""" + # if self.date_range_id: + # self.date_from = self.date_range_id.date_start + # self.date_to = self.date_range_id.date_end + + # @api.constrains("company_id", "date_range_id") + # def _check_company_id_date_range_id(self): + # for rec in self.sudo(): + # if ( + # rec.company_id + # and rec.date_range_id.company_id + # and rec.company_id != rec.date_range_id.company_id + # ): + # raise ValidationError( + # _( + # "The Company in the General Ledger Report Wizard and in " + # "Date Range must be the same." + # ) + # ) + + @api.onchange("receivable_accounts_only", "payable_accounts_only") + def onchange_type_accounts_only(self): + """Handle receivable/payable accounts only change.""" + if self.receivable_accounts_only or self.payable_accounts_only: + domain = [("company_id", "=", self.company_id.id)] + if self.receivable_accounts_only and self.payable_accounts_only: + domain += [ + ("account_type", "in", ("asset_receivable", "liability_payable")) + ] + elif self.receivable_accounts_only: + domain += [("account_type", "=", "asset_receivable")] + elif self.payable_accounts_only: + domain += [("account_type", "=", "liability_payable")] + self.account_ids = self.env["account.account"].search(domain) + else: + self.account_ids = None + + @api.onchange("partner_ids") + def onchange_partner_ids(self): + """Handle partners change.""" + if self.partner_ids: + self.receivable_accounts_only = self.payable_accounts_only = True + else: + self.receivable_accounts_only = self.payable_accounts_only = False + + @api.depends("company_id") + def _compute_unaffected_earnings_account(self): + for record in self: + record.unaffected_earnings_account = self.env["account.account"].search( + [ + ("account_type", "=", "equity_unaffected"), + ("company_id", "=", record.company_id.id), + ] + ) + + unaffected_earnings_account = fields.Many2one( + comodel_name="account.account", + compute="_compute_unaffected_earnings_account", + store=True, + ) + + # def _print_report(self, report_type): + # self.ensure_one() + # data = self._prepare_report_general_ledger() + # report_name = "act_revise.general_ledger" + # return ( + # self.env["ir.actions.report"] + # .search( + # [("report_name", "=", report_name), ("report_type", "=", report_type)], + # limit=1, + # ) + # .report_action(self, data=data) + # ) + def _print_report(self, report_type): + self.ensure_one() + data = self._prepare_report_general_ledger() + report = self.env["ir.actions.report"].search( + [("report_name", "=", "l10n_ru_act_rev.general_ledger"), ("report_type", "=", report_type)], limit=1, ) + + if self.partner_ids[0].parent_id: + partner = int(self.partner_ids[0].parent_id.id) + else: + partner = int(self.partner_ids[0].id) + account_data = self.env['account.move.line'].sudo().search([ + ('partner_id', '=', partner), + ('account_id.account_type', 'in', ('liability_payable', 'asset_receivable')), + ('account_id.non_trade', '=', False), + ('date', '<=', self.date_to), + ('date', '>=', self.date_from) + ]) + logging.warning(f'account_data {account_data}') + if self.target_move == 'posted' and not account_data.filtered(lambda p: p.parent_state == 'posted') or not account_data: + raise UserError(f'Проводок для формирования акта по введенным условиям не найдено.') + return { + 'type': 'ir.actions.act_url', + 'url': '/my/act_revise_contact/%s?date_to=%s&date_from=%s&target_move=%s&company=%s&partner=%s' % ( + urllib.parse.quote(self.get_report_filename()), self.date_to, self.date_from, self.target_move, + self.company_id.id, partner), + 'target': 'new', + } + + def _prepare_report_general_ledger(self): + self.ensure_one() + return { + "wizard_id": self.id, + "date_from": self.date_from, + "date_to": self.date_to, + "only_posted_moves": self.target_move == "posted", + "hide_account_at_0": self.hide_account_at_0, + "foreign_currency": self.foreign_currency, + "company_id": self.company_id.id, + "account_ids": self.account_ids.ids, + "partner_ids": self.partner_ids.ids, + "grouped_by": self.grouped_by, + "cost_center_ids": self.cost_center_ids.ids, + "show_cost_center": self.show_cost_center, + "journal_ids": self.account_journal_ids.ids, + "centralize": self.centralize, + "fy_start_date": self.fy_start_date, + "unaffected_earnings_account": self.unaffected_earnings_account.id, + "account_financial_report_lang": self.env.lang, + "domain": self._get_account_move_lines_domain(), + } + + def _export(self, report_type): + """Default export is PDF.""" + return self._print_report(report_type) + + def _get_atr_from_dict(self, obj_id, data, key): + try: + return data[obj_id][key] + except KeyError: + return data[str(obj_id)][key] + + def numer(self, name): + if name: + numeration = re.findall('\d+$', name) + if numeration: return numeration[0] + return name + + def get_data_format(self, date): + if date and date != 'False': + return dt.ru_strftime(u'%d.%m.%Y г.', date=datetime.strptime(str(date), "%Y-%m-%d"), inflected=True) + return '' + + def initials(self, fio): + if fio: + return (fio.split()[0] + ' ' + ''.join([fio[0:1] + '.' for fio in fio.split()[1:]])).strip() + return '' + + def rubles(self, sum): + "Transform sum number in rubles to text" + text_rubles = numeral.rubles(int(sum)) + copeck = round((sum - int(sum)) * 100) + text_copeck = numeral.choose_plural(int(copeck), (u"копейка", u"копейки", u"копеек")) + return ("%s %02d %s") % (text_rubles, copeck, text_copeck) + + def img(self, img, type='png', width=0, height=0): + if width: + width = "width='%spx'" % (width) + else: + width = " " + if height: + height = "height='%spx'" % (height) + else: + height = " " + toreturn = "" % ( + width, + height, + type, + str(pycompat.to_text(img))) + return toreturn + + def get_contract(self): + partner = int(self.partner_ids[0].id) + contract = self.env['partner.contract.customer'].search( + [('partner_id', '=', partner), ('state', '=', 'signed')], limit=1) + if contract: + return contract + + def get_function_partner(self, partner): + director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) + if director: + if director.function: + return director.function or 'отсутствует' + + def get_name_partner(self, partner): + director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) + if director: + return director.name or 'отсутствует' + + def get_report_filename(self): + today = date.today() + d1 = today.strftime("%d-%m-%Y") + if self.partner_ids[0].parent_id: + p = ''.join(self.partner_ids[0].parent_id.name) + else: + p = ''.join(self.partner_ids[0].name) + # return 'Акт Сверки '+ d1 + ' ' + self.company_id.name+'_'+p + return str(self.company_id.id) + ' - ' + ' ' + d1 + + def sorted_lines(self, list): + list = sorted(list, key=lambda k: k.get('date'), reverse=False) + return list + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +# +# import time +# from ast import literal_eval +# from odoo import _, api, fields, models +# from odoo.tools import date_utils,pycompat +# from pytils import dt,numeral +# from datetime import datetime, date +# import re +# import urllib +# +# class GeneralLedgerReportWizard(models.TransientModel): +# """General ledger report wizard.""" +# +# _name = "general.ledger.act_revise.wizard" +# _description = "General Ledger Report Wizard" +# _inherit = "act_revise.abstract_wizard" +# +# # date_range_id = fields.Many2one(comodel_name="date.range", string="Date range") +# date_from = fields.Date(string="Начало даты", required=True, default=lambda self: self._init_date_from()) +# date_to = fields.Date(string="Конец даты", required=True, default=fields.Date.context_today) +# fy_start_date = fields.Date(compute="_compute_fy_start_date") +# target_move = fields.Selection( +# [("posted", "Р’СЃРµ проведенные РїСЂРѕРІРѕРґРєРё"), ("all", "Р’СЃРµ РїСЂРѕРІРѕРґРєРё")], +# string="Цель операции", +# required=True, +# default="posted", +# ) +# account_ids = fields.Many2many( +# comodel_name="account.account", string=_("Filter accounts") +# ) +# centralize = fields.Boolean(string=_("Activate centralization"), default=True) +# hide_account_at_0 = fields.Boolean( +# string=_("Hide account ending balance at 0"), +# help=_("Use this filter to hide an account or a partner " +# "with an ending balance at 0. " +# "If partners are filtered, " +# "debits and credits totals will not match the trial balance."), +# ) +# receivable_accounts_only = fields.Boolean() +# payable_accounts_only = fields.Boolean() +# partner_ids = fields.Many2many( +# comodel_name="res.partner", +# string=_("Filter partners"), +# default=lambda self: self._default_partners(), +# ) +# account_journal_ids = fields.Many2many( +# comodel_name="account.journal", string=_("Filter journals") +# ) +# cost_center_ids = fields.Many2many( +# comodel_name="account.analytic.account", string=_("Filter cost centers") +# ) +# +# not_only_one_unaffected_earnings_account = fields.Boolean(readonly=True) +# foreign_currency = fields.Boolean( +# string=_("Show foreign currency"), +# help=_("Display foreign currency for move lines, unless " +# "account currency is not setup through chart of accounts " +# "will display initial and final balance in that currency."), +# default=lambda self: self._default_foreign_currency(), +# ) +# account_code_from = fields.Many2one( +# comodel_name="account.account", +# help="Starting account in a range", +# ) +# account_code_to = fields.Many2one( +# comodel_name="account.account", +# help="Ending account in a range", +# ) +# grouped_by = fields.Selection( +# selection=[("", "None"), ("partners", "Partners"), ("taxes", "Taxes")], +# default="partners", +# ) +# show_cost_center = fields.Boolean( +# string="Show Analytic Account", +# default=True, +# ) +# domain = fields.Char( +# string="Journal Items Domain", +# default=[], +# help="This domain will be used to select specific domain for Journal " "Items", +# ) +# +# # def _print_report(self, report_type): +# # self.ensure_one() +# # data = self._prepare_report_general_ledger() +# # report = self.env["ir.actions.report"].search( +# # [("report_name", "=", "act_revise.general_ledger"), ("report_type", "=", report_type)], limit=1, ) +# # if self.partner_ids[0].parent_id: +# # partner = int(self.partner_ids[0].parent_id.id) +# # else: +# # partner = int(self.partner_ids[0].id) +# # return { +# # 'type': 'ir.actions.act_url', +# # 'url': '/my/act_revise_contact/%s?date_to=%s&date_from=%s&target_move=%s&company=%s&partner=%s' % ( +# # urllib.parse.quote(self.get_report_filename()), self.date_to, self.date_from, self.target_move, +# # self.company_id.id, partner), +# # 'target': 'new', +# # } +# +# def _get_account_move_lines_domain(self): +# domain = literal_eval(self.domain) if self.domain else [] +# return domain +# +# @api.onchange("account_code_from", "account_code_to") +# def on_change_account_range(self): +# if ( +# self.account_code_from +# and self.account_code_from.code.isdigit() +# and self.account_code_to +# and self.account_code_to.code.isdigit() +# ): +# start_range = self.account_code_from.code +# end_range = self.account_code_to.code +# self.account_ids = self.env["account.account"].search( +# [("code", ">=", start_range), ("code", "<=", end_range)] +# ) +# if self.company_id: +# self.account_ids = self.account_ids.filtered( +# lambda a: a.company_id == self.company_id +# ) +# +# def _init_date_from(self): +# """set start date to begin of current year if fiscal year running""" +# today = fields.Date.context_today(self) +# company = self.company_id or self.env.company +# last_fsc_month = company.fiscalyear_last_month +# last_fsc_day = company.fiscalyear_last_day +# +# if ( +# today.month < int(last_fsc_month) +# or today.month == int(last_fsc_month) +# and today.day <= last_fsc_day +# ): +# return time.strftime("%Y-01-01") +# else: +# return False +# +# def _default_foreign_currency(self): +# return self.env.user.has_group("base.group_multi_currency") +# +# @api.depends("date_from") +# def _compute_fy_start_date(self): +# for wiz in self: +# if wiz.date_from: +# date_from, date_to = date_utils.get_fiscal_year( +# wiz.date_from, +# day=self.company_id.fiscalyear_last_day, +# month=int(self.company_id.fiscalyear_last_month), +# ) +# wiz.fy_start_date = date_from +# else: +# wiz.fy_start_date = False +# +# @api.onchange("company_id") +# def onchange_company_id(self): +# """Handle company change.""" +# count = self.env["account.account"].search_count( +# [ +# ("account_type", "=", "equity_unaffected"), +# ("company_id", "=", self.company_id.id), +# ] +# ) +# self.not_only_one_unaffected_earnings_account = count != 1 +# # if ( +# # self.company_id +# # and self.date_range_id.company_id +# # and self.date_range_id.company_id != self.company_id +# # ): +# # self.date_range_id = False +# if self.company_id and self.account_journal_ids: +# self.account_journal_ids = self.account_journal_ids.filtered( +# lambda p: p.company_id == self.company_id or not p.company_id +# ) +# if self.company_id and self.partner_ids: +# self.partner_ids = self.partner_ids.filtered( +# lambda p: p.company_id == self.company_id or not p.company_id +# ) +# if self.company_id and self.account_ids: +# if self.receivable_accounts_only or self.payable_accounts_only: +# self.onchange_type_accounts_only() +# else: +# self.account_ids = self.account_ids.filtered( +# lambda a: a.company_id == self.company_id +# ) +# if self.company_id and self.cost_center_ids: +# self.cost_center_ids = self.cost_center_ids.filtered( +# lambda c: c.company_id == self.company_id +# ) +# res = { +# "domain": { +# "account_ids": [], +# "partner_ids": [], +# "account_journal_ids": [], +# "cost_center_ids": [], +# # "date_range_id": [], +# } +# } +# if not self.company_id: +# return res +# else: +# res["domain"]["account_ids"] += [("company_id", "=", self.company_id.id)] +# res["domain"]["account_journal_ids"] += [ +# ("company_id", "=", self.company_id.id) +# ] +# res["domain"]["partner_ids"] += self._get_partner_ids_domain() +# res["domain"]["cost_center_ids"] += [ +# ("company_id", "=", self.company_id.id) +# ] +# # res["domain"]["date_range_id"] += [ +# # "|", +# # ("company_id", "=", self.company_id.id), +# # ("company_id", "=", False), +# # ] +# return res +# +# # @api.onchange("date_range_id") +# # def onchange_date_range_id(self): +# # """Handle date range change.""" +# # if self.date_range_id: +# # self.date_from = self.date_range_id.date_start +# # self.date_to = self.date_range_id.date_end +# +# # @api.constrains("company_id", "date_range_id") +# # def _check_company_id_date_range_id(self): +# # for rec in self.sudo(): +# # if ( +# # rec.company_id +# # and rec.date_range_id.company_id +# # and rec.company_id != rec.date_range_id.company_id +# # ): +# # raise ValidationError( +# # _( +# # "The Company in the General Ledger Report Wizard and in " +# # "Date Range must be the same." +# # ) +# # ) +# +# @api.onchange("receivable_accounts_only", "payable_accounts_only") +# def onchange_type_accounts_only(self): +# """Handle receivable/payable accounts only change.""" +# if self.receivable_accounts_only or self.payable_accounts_only: +# domain = [("company_id", "=", self.company_id.id)] +# if self.receivable_accounts_only and self.payable_accounts_only: +# domain += [ +# ("account_type", "in", ("asset_receivable", "liability_payable")) +# ] +# elif self.receivable_accounts_only: +# domain += [("account_type", "=", "asset_receivable")] +# elif self.payable_accounts_only: +# domain += [("account_type", "=", "liability_payable")] +# self.account_ids = self.env["account.account"].search(domain) +# else: +# self.account_ids = None +# +# @api.onchange("partner_ids") +# def onchange_partner_ids(self): +# """Handle partners change.""" +# if self.partner_ids: +# self.receivable_accounts_only = self.payable_accounts_only = True +# else: +# self.receivable_accounts_only = self.payable_accounts_only = False +# +# @api.depends("company_id") +# def _compute_unaffected_earnings_account(self): +# for record in self: +# record.unaffected_earnings_account = self.env["account.account"].search( +# [ +# ("account_type", "=", "equity_unaffected"), +# ("company_id", "=", record.company_id.id), +# ] +# ) +# +# unaffected_earnings_account = fields.Many2one( +# comodel_name="account.account", +# compute="_compute_unaffected_earnings_account", +# store=True, +# ) +# +# # def _print_report(self, report_type): +# # self.ensure_one() +# # data = self._prepare_report_general_ledger() +# # report_name = "act_revise.general_ledger" +# # return ( +# # self.env["ir.actions.report"] +# # .search( +# # [("report_name", "=", report_name), ("report_type", "=", report_type)], +# # limit=1, +# # ) +# # .report_action(self, data=data) +# # ) +# def _print_report(self, report_type): +# self.ensure_one() +# data = self._prepare_report_general_ledger() +# report = self.env["ir.actions.report"].search( +# [("report_name", "=", "act_revise.general_ledger"), ("report_type", "=", report_type)], limit=1, ) +# # report.report_name='Test' +# # report.headers.add('Content-Disposition', 'attachment; filename="Test.pdf";') +# # pdf, _ = request.env.ref('act_revise.action_print_report_general_ledger_qweb').sudo().render_qweb_pdf(self,data=data) +# # pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf)),('Content-Disposition', 'attachment; filename="%s";' % report_name),] +# # return request.make_response(pdf, headers=pdfhttpheaders) +# # return ( +# # self.env["ir.actions.report"] +# # .search( +# # [("report_name", "=", "act_revise.general_ledger"), ("report_type", "=", report_type)], +# # limit=1, +# # ) +# # report.report_action(self, data=data) +# # ) +# # name='Test' +# if self.partner_ids[0].parent_id: +# partner = int(self.partner_ids[0].parent_id.id) +# else: +# partner = int(self.partner_ids[0].id) +# account_data = self.env['account.move.line'].sudo().search([ +# ('partner_id', '=', partner.id), +# ('account_id.account_type', 'in', ('liability_payable', 'asset_receivable')), +# ('account_id.non_trade', '=', False), +# ('date', '<=', self.date_to), +# ('date', '>=', self.date_from) +# ]) +# if not account_data: +# raise UserError(f'Проводок для формирования акта по введенным условиям не найдено.') +# return { +# 'type': 'ir.actions.act_url', +# 'url': '/my/act_revise_contact/%s?date_to=%s&date_from=%s&target_move=%s&company=%s&partner=%s' % ( +# urllib.parse.quote(self.get_report_filename()), self.date_to, self.date_from, self.target_move, +# self.company_id.id, partner), +# 'target': 'new', +# } +# +# def _prepare_report_general_ledger(self): +# self.ensure_one() +# return { +# "wizard_id": self.id, +# "date_from": self.date_from, +# "date_to": self.date_to, +# "only_posted_moves": self.target_move == "posted", +# "hide_account_at_0": self.hide_account_at_0, +# "foreign_currency": self.foreign_currency, +# "company_id": self.company_id.id, +# "account_ids": self.account_ids.ids, +# "partner_ids": self.partner_ids.ids, +# "grouped_by": self.grouped_by, +# "cost_center_ids": self.cost_center_ids.ids, +# "show_cost_center": self.show_cost_center, +# "journal_ids": self.account_journal_ids.ids, +# "centralize": self.centralize, +# "fy_start_date": self.fy_start_date, +# "unaffected_earnings_account": self.unaffected_earnings_account.id, +# "account_financial_report_lang": self.env.lang, +# "domain": self._get_account_move_lines_domain(), +# } +# +# def _export(self, report_type): +# """Default export is PDF.""" +# return self._print_report(report_type) +# +# def _get_atr_from_dict(self, obj_id, data, key): +# try: +# return data[obj_id][key] +# except KeyError: +# return data[str(obj_id)][key] +# +# def numer(self, name): +# if name: +# numeration = re.findall('\d+$', name) +# if numeration: return numeration[0] +# return name +# +# def get_data_format(self, date): +# if date and date != 'False': +# return dt.ru_strftime(u'%d.%m.%Y Рі.', date=datetime.strptime(str(date), "%Y-%m-%d"), inflected=True) +# return '' +# +# def initials(self, fio): +# if fio: +# return (fio.split()[0] + ' ' + ''.join([fio[0:1] + '.' for fio in fio.split()[1:]])).strip() +# return '' +# +# def rubles(self, sum): +# "Transform sum number in rubles to text" +# text_rubles = numeral.rubles(int(sum)) +# copeck = round((sum - int(sum)) * 100) +# text_copeck = numeral.choose_plural(int(copeck), (u"копейка", u"копейки", u"копеек")) +# return ("%s %02d %s") % (text_rubles, copeck, text_copeck) +# +# def img(self, img, type='png', width=0, height=0): +# if width: +# width = "width='%spx'" % (width) +# else: +# width = " " +# if height: +# height = "height='%spx'" % (height) +# else: +# height = " " +# toreturn = "" % ( +# width, +# height, +# type, +# str(pycompat.to_text(img))) +# return toreturn +# +# def get_contract(self): +# partner = int(self.partner_ids[0].id) +# contract = self.env['partner.contract.customer'].search( +# [('partner_id', '=', partner), ('state', '=', 'signed')], limit=1) +# if contract: +# return contract +# +# def get_function_partner(self, partner): +# director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) +# if director: +# if director.function: +# return director.function or 'отсутствует' +# +# def get_name_partner(self, partner): +# director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) +# if director: +# return director.name or 'отсутствует' +# +# def get_report_filename(self): +# today = date.today() +# d1 = today.strftime("%d-%m-%Y") +# if self.partner_ids[0].parent_id: +# p = ''.join(self.partner_ids[0].parent_id.name) +# else: +# p = ''.join(self.partner_ids[0].name) +# # return 'РђРєС‚ Сверки '+ d1 + ' ' + self.company_id.name+'_'+p +# return str(self.company_id.id) + ' - ' + ' ' + d1 +# +# def sorted_lines(self, list): +# list = sorted(list, key=lambda k: k.get('date'), reverse=False) +# return list diff --git a/l10n_ru_act_rev/wizard/general_ledger_wizard_view.xml b/l10n_ru_act_rev/wizard/general_ledger_wizard_view.xml new file mode 100644 index 0000000..7ea248f --- /dev/null +++ b/l10n_ru_act_rev/wizard/general_ledger_wizard_view.xml @@ -0,0 +1,167 @@ + + + + + Акт сверки + general.ledger.act_revise.wizard + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +

+ General Ledger can be computed only if selected company have + only one unaffected earnings account. +

+ +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + Акт сверки + general.ledger.act_revise.wizard + form + + new + + + + Печатать акт сверки + general.ledger.act_revise.wizard + + form + {'default_receivable_accounts_only': 1, 'default_payable_accounts_only': 1} + + new + + + + +
diff --git a/l10n_ru_attorney/README.md b/l10n_ru_attorney/README.md new file mode 100644 index 0000000..58c8d91 --- /dev/null +++ b/l10n_ru_attorney/README.md @@ -0,0 +1,21 @@ +# Российская локализация - Доверенность +name: l10n_ru_attorney + +## Описание +Создание списка доверенностей на получение ТМЦ и их печать. + +###Создание доверенности: +1. Меню Покупки - Доверенности - кнопка "Создать"; +2. На форме указываем: + + 2.1. Контрагент - поставщик; + + 2.2. Заказ на закупку; + + 2.3. Даты действия доверенности ("дата выдачи" и "действительно по"). + + +###Для печати: +1. Меню Настройки - Техническое - Отчеты; +2. Находим в списке l10n_ru_attorney и добавляем в меню "Печать"; +3. Открываем созданную запись доверенности - Действие - "Доверенность". \ No newline at end of file diff --git a/l10n_ru_attorney/__init__.py b/l10n_ru_attorney/__init__.py new file mode 100644 index 0000000..5305644 --- /dev/null +++ b/l10n_ru_attorney/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/l10n_ru_attorney/__manifest__.py b/l10n_ru_attorney/__manifest__.py new file mode 100644 index 0000000..b076846 --- /dev/null +++ b/l10n_ru_attorney/__manifest__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Российская локализация - Доверенность", + + 'summary': """ + Печать доверенности на получение ТМЦ + """, + + 'description': """ + Создание списка доверенностей на получение ТМЦ и их печать. + + Создание доверенности: + 1. Меню Покупки - Доверенности - кнопка "Создать"; + 2. На форме указываем: + 2.1. Контрагент - поставщик; + 2.2. Заказ на закупку; + 2.3. Даты действия доверенности ("дата выдачи" и "действительно по"). + + Для печати: + 1. Меню Настройки - Техническое - Отчеты; + 2. Находим в списке l10n_ru_attorney и добавляем в меню "Печать"; + 3. Открываем созданную запись доверенности - Действие - "Доверенность". + """, + + 'author': "MK.Lab", + 'website': "https://www.inf-centre.ru/", + + 'category': 'Uncategorized', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base', 'account', 'sale', 'purchase', 'hr', 'l10n_ru_base'], + + # always loaded + 'data': [ + 'security/ir.model.access.csv', + 'views/base_consent_views.xml', + 'views/hr_employee_views.xml', + 'views/purchase_order_views.xml', + 'report/consent_report.xml', + ], +} diff --git a/l10n_ru_attorney/models/__init__.py b/l10n_ru_attorney/models/__init__.py new file mode 100644 index 0000000..0cad714 --- /dev/null +++ b/l10n_ru_attorney/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import base_consent +from . import hr_employee +from . import purchase_order \ No newline at end of file diff --git a/l10n_ru_attorney/models/base_consent.py b/l10n_ru_attorney/models/base_consent.py new file mode 100644 index 0000000..70df0bf --- /dev/null +++ b/l10n_ru_attorney/models/base_consent.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ +from datetime import datetime, timedelta + + +class BaseConsent(models.Model): + _name = 'base.consent' + _inherit = ['mail.thread', 'utm.mixin'] + _description = 'Consent' + _order = 'date_from desc' + + name = fields.Char(string=_('Номер')) + date_from = fields.Date(string=_('Дата выдачи'), default=lambda self: fields.Datetime.now()) + date_to = fields.Date(string=_('Действительна по'), default=lambda self: datetime.today() + timedelta(days=180)) + partner_id = fields.Many2one('res.partner', string=_('Контрагент'), required=1) + employee_id = fields.Many2one('hr.employee', string=_('Сотрудник'), required=1) + purchaseorder_id = fields.Many2one('purchase.order', _('Заказ на закупку'), domain="[('partner_id','=',partner_id)]", + required=1) + company_id = fields.Many2one('res.company', string=_('Компания'), + default=lambda self: self.env['res.company']._company_default_get('base.consent'), + required=1) + + @api.model + def create(self, val): + name = self.env['ir.sequence'].next_by_code('base.consent') + if name: + if 'name' in val: + if val['name'] == False: + val.update({ + 'name': name, + }) + + result = super(BaseConsent, self).create(val) + return result + + @api.onchange('purchaseorder_id') + def set_partner(self): + if self.purchaseorder_id: + self.partner_id = self.purchaseorder_id.partner_id + + @api.constrains('purchaseorder_id') + def fill_order(self): + p_orders = self.env['purchase.order'].sudo().browse(self.purchaseorder_id.id) + for order in p_orders: + order.consent_id = self.id diff --git a/l10n_ru_attorney/models/hr_employee.py b/l10n_ru_attorney/models/hr_employee.py new file mode 100644 index 0000000..1e63258 --- /dev/null +++ b/l10n_ru_attorney/models/hr_employee.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models, _ + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + inn = fields.Char(string=_("ИНН")) + pass_kem = fields.Char(string=_("Кем выдан паспорт")) + pass_date = fields.Date(string=_('Дата выдачи паспорта')) diff --git a/l10n_ru_attorney/models/purchase_order.py b/l10n_ru_attorney/models/purchase_order.py new file mode 100644 index 0000000..aa178c6 --- /dev/null +++ b/l10n_ru_attorney/models/purchase_order.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models, _ + + +class PurchaseOrder(models.Model): + _inherit = 'purchase.order' + + consent_id = fields.Many2one('base.consent', string=_('Доверенность')) diff --git a/l10n_ru_attorney/report/consent_report.xml b/l10n_ru_attorney/report/consent_report.xml new file mode 100644 index 0000000..d2dc293 --- /dev/null +++ b/l10n_ru_attorney/report/consent_report.xml @@ -0,0 +1,560 @@ + + + + + + + Доверенность + base.consent + (u'Доверенность - %s.pdf' % (object.name)) + qweb-html + l10n_ru_attorney.report_consent + + + + A4 + + A4 + 0 + 0 + Portrait + 15 + 15 + 7 + 7 + + 10 + 90 + + + + + diff --git a/l10n_ru_attorney/security/ir.model.access.csv b/l10n_ru_attorney/security/ir.model.access.csv new file mode 100644 index 0000000..ee8128f --- /dev/null +++ b/l10n_ru_attorney/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_base_consent,base.consent,model_base_consent,base.group_user,1,1,1,1 diff --git a/l10n_ru_attorney/views/base_consent_views.xml b/l10n_ru_attorney/views/base_consent_views.xml new file mode 100644 index 0000000..db8dc9a --- /dev/null +++ b/l10n_ru_attorney/views/base_consent_views.xml @@ -0,0 +1,65 @@ + + + + + Consents + base.consent + + + + + + + + + + + + + consent.form + base.consent + +
+
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + Доверенности + base.consent + tree,form + + + + + + Consents + base.consent + CON + 5 + + + +
+
diff --git a/l10n_ru_attorney/views/hr_employee_views.xml b/l10n_ru_attorney/views/hr_employee_views.xml new file mode 100644 index 0000000..440c5d4 --- /dev/null +++ b/l10n_ru_attorney/views/hr_employee_views.xml @@ -0,0 +1,19 @@ + + + + + view_employee_form.inherit + hr.employee + + + + + + + + + + + + + diff --git a/l10n_ru_attorney/views/purchase_order_views.xml b/l10n_ru_attorney/views/purchase_order_views.xml new file mode 100644 index 0000000..4003990 --- /dev/null +++ b/l10n_ru_attorney/views/purchase_order_views.xml @@ -0,0 +1,16 @@ + + + + + purchase.order.form.inherit + purchase.order + + + + + + + + + diff --git a/l10n_ru_base/README.md b/l10n_ru_base/README.md new file mode 100644 index 0000000..d8b931c --- /dev/null +++ b/l10n_ru_base/README.md @@ -0,0 +1,16 @@ +# Российская локализация - Базовый +name: l10n_ru_base + +## Описание +Российская локализация: основные отчеты и печатные формы. Это базовый модуль для работы с модулями локализации. + +###Для включения модулей: +1. Меню Настройки - в боковом меню "Российская локализация"; +2. Выбирается нужный блок для подключения дополнительных возможностей по локализации. + +###Перечень модулей локализации: +1. Российская локализация - Акт сверки (l10n_ru_act_rev) +2. Российская локализация - Доверенность (l10n_ru_attorney) +3. Российская локализация - Договоры (l10n_ru_contract) +4. Российская локализация - Документы (l10n_ru_doc) +5. Российская локализация - УПД в xml-формате (l10n_ru_upd_xml) diff --git a/l10n_ru_base/__init__.py b/l10n_ru_base/__init__.py new file mode 100644 index 0000000..899bcc9 --- /dev/null +++ b/l10n_ru_base/__init__.py @@ -0,0 +1,2 @@ +from . import models + diff --git a/l10n_ru_base/__manifest__.py b/l10n_ru_base/__manifest__.py new file mode 100644 index 0000000..45f2703 --- /dev/null +++ b/l10n_ru_base/__manifest__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Российская локализация - Базовый>", + 'summary': """ + Российская локализация: основные отчеты и печатные формы. + """, + + 'description': """ + Российская локализация: основные отчеты и печатные формы. + + Для включения модулей: + 1. Меню Настройки - в боковом меню "Российская локализация" + 2. Выбирается нужный блок. + """, + + 'author': "MK.Lab", + 'website': "https://www.inf-centre.ru/", + + 'version': '17.0.1.0.0', + 'license': 'LGPL-3', + 'category': 'Uncategorized', + + 'depends': [], + + 'data': [ + 'views/res_config_settings_views.xml', + ], +} diff --git a/l10n_ru_base/i18n/ru_RU.po b/l10n_ru_base/i18n/ru_RU.po new file mode 100644 index 0000000..4e0f84e --- /dev/null +++ b/l10n_ru_base/i18n/ru_RU.po @@ -0,0 +1,109 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_russian_localization +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e-20240904\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-10-29 07:45+0000\n" +"PO-Revision-Date: 2024-10-29 07:45+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_russian_localization +#. odoo-python +#: code:addons/base_russian_localization/models/res_config_settings.py:0 +#: model:ir.model.fields,field_description:base_russian_localization.field_res_config_settings__module_act_revise +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +#, python-format +msgid "Act revise" +msgstr "Акт сверки" + +#. module: base_russian_localization +#: model:ir.model.fields,field_description:base_russian_localization.field_res_config_settings__company_status_rf +msgid "Company Status Rf" +msgstr "Статус компании" + +#. module: base_russian_localization +#: model:ir.model,name:base_russian_localization.model_res_config_settings +msgid "Config Settings" +msgstr "Параметры конфигурации" + +#. module: base_russian_localization +#. odoo-python +#: code:addons/base_russian_localization/models/res_config_settings.py:0 +#: model:ir.model.fields,field_description:base_russian_localization.field_res_config_settings__module_fehu_base_consent +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +#, python-format +msgid "Consent" +msgstr "Доверенность" + +#. module: base_russian_localization +#. odoo-python +#: code:addons/base_russian_localization/models/res_config_settings.py:0 +#: model:ir.model.fields,field_description:base_russian_localization.field_res_config_settings__module_contract +#, python-format +msgid "Contract" +msgstr "Договор" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "Contracts" +msgstr "Договоры" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "Powers of attorney" +msgstr "Доверенность на получение ТМЦ" + +#. module: base_russian_localization +#. odoo-python +#: code:addons/base_russian_localization/models/res_config_settings.py:0 +#: model:ir.model.fields,field_description:base_russian_localization.field_res_config_settings__module_l10n_ru_doc +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +#, python-format +msgid "Print forms" +msgstr "Печатные формы" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "Report act revise" +msgstr "Отчет Акт сверки" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "Report contract" +msgstr "Договоры" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "Report report_xml" +msgstr "УПД в формате xml для ЭДО" + +#. module: base_russian_localization +#. odoo-python +#: code:addons/base_russian_localization/models/res_config_settings.py:0 +#: model:ir.model.fields,field_description:base_russian_localization.field_res_config_settings__module_report_xml +#, python-format +msgid "Report_xml" +msgstr "Универсальный передаточный документ (УПД) в формате xml для ЭДО" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "Russian Localization" +msgstr "Российская локализация" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "UPD xml" +msgstr "УПД в формате xml для ЭДО" + +#. module: base_russian_localization +#: model_terms:ir.ui.view,arch_db:base_russian_localization.res_config_settings_view_form +msgid "print_forms" +msgstr "Печатные формы документов" diff --git a/l10n_ru_base/models/__init__.py b/l10n_ru_base/models/__init__.py new file mode 100644 index 0000000..4a30401 --- /dev/null +++ b/l10n_ru_base/models/__init__.py @@ -0,0 +1,4 @@ +from . import res_config_settings + + + diff --git a/l10n_ru_base/models/res_config_settings.py b/l10n_ru_base/models/res_config_settings.py new file mode 100644 index 0000000..60e3061 --- /dev/null +++ b/l10n_ru_base/models/res_config_settings.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_FIELDS_MODUL = [ + 'module_l10n_ru_act_rev', + 'module_l10n_ru_contract', + 'module_l10n_ru_upd_xml', + 'module_l10n_ru_doc', + 'module_l10n_ru_attorney' +] + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + module_l10n_ru_act_rev = fields.Boolean(_("Act revise")) + module_l10n_ru_contract = fields.Boolean(_("Contract")) + module_l10n_ru_upd_xml = fields.Boolean(_("Report_xml")) + module_l10n_ru_doc = fields.Boolean(_("Print forms")) + module_l10n_ru_attorney = fields.Boolean(_("Consent")) + + @api.model + def write(self, values): + company = self.env.company + if company.country_id.code != 'RU': + raise UserError("Признак Российской компании не обнаружен!") + if _FIELDS_MODUL: + missing_modules = set() + for field in _FIELDS_MODUL: + if self.mapped(field)[0]: + module_name = field[7:] + module_installed = self.env['ir.module.module'].search([('name', '=', module_name)], limit=1) + if not module_installed: + missing_modules.add(module_name) + if missing_modules: + message = "Обратитесь в тех.поддержку для получения лицензии для следующих модулей:\n" + \ + "\n".join(missing_modules) + raise UserError(message) + return super(ResConfigSettings, self).write(values) diff --git a/l10n_ru_base/static/description/icon.png b/l10n_ru_base/static/description/icon.png new file mode 100644 index 0000000..5ea24f8 Binary files /dev/null and b/l10n_ru_base/static/description/icon.png differ diff --git a/l10n_ru_base/views/res_config_settings_views.xml b/l10n_ru_base/views/res_config_settings_views.xml new file mode 100644 index 0000000..c60513a --- /dev/null +++ b/l10n_ru_base/views/res_config_settings_views.xml @@ -0,0 +1,42 @@ + + + + + res.config.settings.view.form.inherit.russian.localization + res.config.settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_ru_contract/__init__.py b/l10n_ru_contract/__init__.py new file mode 100644 index 0000000..7db6694 --- /dev/null +++ b/l10n_ru_contract/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import models +from . import report diff --git a/l10n_ru_contract/__manifest__.py b/l10n_ru_contract/__manifest__.py new file mode 100644 index 0000000..c1ad9b9 --- /dev/null +++ b/l10n_ru_contract/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Контракты клиентов', + 'version': '17.0.2024.02.02', + 'sequence': 0, + 'author': 'StarlingSoft', + 'website': 'https://inf-centre.ru', + 'depends': [ + 'base', + 'mail', + 'account', 'sale', 'sale_management', 'purchase' + ], + 'data': [ + 'data/data.xml', + 'views/contract_customer_view.xml', + 'security/ir.model.access.csv', + 'report/report_contract.xml', + 'report/report_contract_order.xml', + 'report/report_contract_order1.xml', + 'report/report_contract_invoce.xml', + + ], + 'installable': True, + 'auto_install': False, +} diff --git a/l10n_ru_contract/data/data.xml b/l10n_ru_contract/data/data.xml new file mode 100644 index 0000000..3b1bf11 --- /dev/null +++ b/l10n_ru_contract/data/data.xml @@ -0,0 +1,21 @@ + + + + + Договор последовательность клиент + partner.contract.customer.sequence + + + + 5 + + + Договор последовательность поставщик + partner.contract.supplier.sequence + + + + 5 + + + diff --git a/l10n_ru_contract/models/__init__.py b/l10n_ru_contract/models/__init__.py new file mode 100644 index 0000000..837d2e2 --- /dev/null +++ b/l10n_ru_contract/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import contract_customer, invoice_saleorder, dop_field, crutch_fields_header diff --git a/l10n_ru_contract/models/contract_customer.py b/l10n_ru_contract/models/contract_customer.py new file mode 100644 index 0000000..a9464d0 --- /dev/null +++ b/l10n_ru_contract/models/contract_customer.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, exceptions, tools +import pymorphy2 +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +from .crutch_fields_header import IP_CONTACT_HEADER, ENTITY_CONTRACT_HEADER, INDIVIDUAL_CONTRACT_HEADER + + +class PartnerContractCustomer(models.Model): + _name = 'partner.contract.customer' + _inherit = ['mail.thread', 'mail.activity.mixin', 'mail.render.mixin'] + + def get_dateend(self): + if self.date_start: + six_months = fields.Datetime.from_string(self.date_start) + relativedelta(months=+11) + else: + six_months = datetime.today() + relativedelta(months=+11) + return fields.Datetime.to_string(six_months) + + name = fields.Char(string='Номер') + date_start = fields.Date(string='Дата договора', required=True, default=fields.Datetime.now()) + partner_id = fields.Many2one('res.partner', string='Контрагент', required=True) + sec_partner_id = fields.Many2one('res.partner', string='Контрагент как в заказе') + company_id = fields.Many2one('res.company', string='Компания', required=True) + name_print = fields.Char(string='Имя для печати', compute='_get_name_print') + name_print1 = fields.Char(string='Имя для печати, И.П.', compute='_get_name_printip') + date_end = fields.Date(string='Дата окончания', required=True, default=get_dateend) + name_dirprint = fields.Char(string='Имя нашего директора для печати') + name_dirprint1 = fields.Char(string='Имя нашего директора для печати И.П.') + lines = fields.One2many('contract.line', 'contract_id', string='Договорные цены') + type = fields.Selection( + [('customer', 'С покупателем'), + ('supplier', 'С поставщиком'), + ('other', 'Прочие расчеты'), + + ], + string='Тип договора', default='customer', required=True) + saleorder_id = fields.Many2one('sale.order', string='Заказ/Сделка') + stamp = fields.Boolean(string='Печать и подпись') + signed = fields.Boolean(string='Договор подписан') + state = fields.Selection( + [('draft', 'Черновик'), + ('progress', 'На согласовании'), + ('signed', 'Подписан, действует'), + ('closed', 'Истёк'), + ], + string='Статус', default='draft', group_expand='_expand_states', index=True + ) + is_template = fields.Boolean('Это шаблон') + copy_from = fields.Many2one('partner.contract.customer', string='Копировать из этого шаблона') + profile_id = fields.Many2one('contract.profile', string='Вид договора', required=True) + credit_limit = fields.Float(string='Лимит кредита') + guid_1s = fields.Char('Код договора из 1С') + buh_code = fields.Char('Код договора из бухгалтерии') + payment_term_id = fields.Many2one('account.payment.term', string='Условие оплаты') + manager_id = fields.Many2one('res.users', string='Менеджер по продажам') + accountant_id = fields.Many2one('res.users', string='Бухгалтер по взаиморасчетам') + time_to_delivery_from = fields.Datetime('Время доставки от') + time_to_delivery_to = fields.Datetime('Время доставки до') + day_of_delivery = fields.Float('Дни доставки') + day_of_otgruzki = fields.Float('Дни отгрузки') + + channel_id = fields.Many2one('saleorder.channel', string='Канал продаж') + team_id = fields.Many2one('crm.team', string='Команда продаж') + order_days_ids = fields.Many2many(comodel_name='contract.day', relation='orderdays', string='Дни доставки', + column1='contract_id', column2='day_id') + shipment_days_ids = fields.Many2many(comodel_name='contract.day', relation='shipmentdays', string='Дни отгрузки', + column1='contract_id', + column2='day_id') + # Доработка хедера договора + partner_type = fields.Selection(string='Тип контрагента', selection=[ + ('person', 'Физ. лицо'), + ('company_ip', 'ИП'), + ('company', 'Юр. лицо') + ], required=True) + contract_header = fields.Html('Шапка договора') + + @api.onchange('partner_type') + def generate_contract_header(self): + self.ensure_one() + self.render_model = 'partner.contract.customer' + if self.partner_type == 'company_ip': + self.contract_header = IP_CONTACT_HEADER + elif self.partner_type == 'person': + self.contract_header = INDIVIDUAL_CONTRACT_HEADER + else: + self.contract_header = ENTITY_CONTRACT_HEADER + # # Рендер Jinja выражение типа {{object.field}} + result = self._render_template(self.contract_header, self.render_model, res_ids=[self.id]) + result = tools.html_sanitize(result[self.id]) + self.contract_header = result + + @api.onchange('sec_partner_id') + def set_pid(self): + self.partner_id = self.sec_partner_id.parent_id if self.sec_partner_id.parent_id else self.sec_partner_id + + def _expand_states(self, states, domain, order): + return [key for key, val in type(self).state.selection] + + def copy_it(self): + if self.copy_from: + for line in self.copy_from.lines: + line.copy({'contract_id': self.id}) + + def _get_name_print(self): + morph = pymorphy2.MorphAnalyzer() + self.name_print = False + director = self.env['res.partner'].search([('parent_id', '=', self.partner_id.id), ('type', '=', 'director')], + limit=1) + if director: + if len(director.name.split(' ')) == 3: + lastname_old, firstname_old, middlename_old = director.name.split(' ') + + if lastname_old: + lastname_n = morph.parse(lastname_old)[0] + if lastname_n.inflect({'gent'}): + lastname_n = lastname_n.inflect({'gent'}).word + else: + lastname_n = lastname_old + else: + lastname_n = '' + + if firstname_old: + firstname_n = morph.parse(firstname_old)[0] + firstname_n = firstname_n.inflect({'gent'}).word + else: + firstname_n = '' + + if middlename_old: + middlename_n = morph.parse(middlename_old)[0] + middlename_n = middlename_n.inflect({'gent'}).word + else: + middlename_n = '' + + name_print = lastname_n + ' ' + firstname_n + ' ' + middlename_n + + self.name_print = name_print.title() + + def _get_name_print1(self): + # morph = pymorphy2.MorphAnalyzer() + director = self.company_id.chief_id.partner_id if self.company_id.chief_id else False + # raise exceptions.UserError(str(director)) + self.name_dirprint = False + if director: + if len(director.name.split(' ')) == 3: + lastname_old, firstname_old, middlename_old = director.name.split(' ') + + if lastname_old: + lastname_n = morph.parse(lastname_old)[0] + lastname_n = lastname_n.inflect({'gent'}).word + else: + lastname_n = '' + + if firstname_old: + firstname_n = morph.parse(firstname_old)[0] + firstname_n = firstname_n.inflect({'gent'}).word + else: + firstname_n = '' + + if middlename_old: + middlename_n = morph.parse(middlename_old)[0] + middlename_n = middlename_n.inflect({'gent'}).word + else: + middlename_n = '' + + name_print = lastname_n + ' ' + firstname_n + ' ' + middlename_n + + self.name_dirprint = name_print.title() + + @api.model + def create(self, values): + res = super(PartnerContractCustomer, self).create(values) + + if values.get('is_template'): + return res + + if values.get('type') == 'customer': + sequence_code = 'partner.contract.customer.sequence' + elif values.get('type') == 'supplier': + sequence_code = 'partner.contract.supplier.sequence' + else: + return res + + name = self.env['ir.sequence'].next_by_code(sequence_code) + + res.update({ + 'name': name, + }) + + return res + + # @api.model + def write(self, values): + + if 'state' in values: + if self.state != values['state']: + msg = 'Статус: ' + dict(self._fields['state'].selection).get(self.state) + ' -> ' + dict( + self._fields['state'].selection).get(values['state']) + self.message_post(body=msg) + res = super(PartnerContractCustomer, self).write(values) + return res + + def _get_name_print1ip(self): + self.name_dirprint1 = self.company_id.chief_id.partner_id.name if self.company_id.chief_id else False + + def _get_name_printip(self): + self.name_print1 = False + director = self.env['res.partner'].search([('parent_id', '=', self.partner_id.id), ('type', '=', 'director')], + limit=1) + if director: + self.name_print1 = director.name + + def print_supp(self): + # self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'}) + return self.env['report'].get_action(self, 'mta_base.action_mtacontractsupp_report') + + def print_contract_cust(self): + if self.saleorder_id: + return self.saleorder_id.print_contract() + else: + raise exceptions.UserError( + 'Вы не можете напечатать договор с Клиентом, потому что нет связи с Заказом. Нужно зайти в Заказ и привязать этот договор.') + + def contract_action_confirm(self): + if self.state == 'draft': + self.state = 'progress' + elif self.state == 'progress': + self.state = 'signed' + + def contract_in_draft(self): + self.state = 'draft' + + @api.onchange('name') + def set_comp_and_partn(self): + context = self._context + order_id = context.get('sale_order_id') + if order_id: + sale_order = self.env['sale.order'].browse(order_id) + + self.company_id = sale_order.company_id + self.partner_id = sale_order.partner_id + + @api.onchange('profile_id') + def set_payment(self): + if self.profile_id.payment_term_id: + self.payment_term_id = self.profile_id.payment_term_id + + # @api.constrains('name') + def check_name(self): + obj = self.search([('name', '=', self.name), ('id', '!=', self.id), ('state', '!=', 'closed')]) + if obj: + raise exceptions.ValidationError('Договор с таким номером уже существует') + + """ + @api.constrains('profile_id') + def check_profile_id(self): + contracts = self.search([('partner_id', '=', self.partner_id.id), ('id', '!=', self.id)]) + if contracts: + profiles_in_contracts = contracts.profile_id + # raise exceptions.ValidationError(profiles_in_contracts.ids) + if profiles_in_contracts: + ads = self.env['contract.allowed.profiles'].search( + [('allowed_profiles', 'in', profiles_in_contracts.ids)]) + if ads: + raise exceptions.ValidationError((self.profile_id.name, ads.name)) + # raise exceptions.ValidationError(contracts) + """ + + +class Partner(models.Model): + _inherit = 'res.partner' + + contract_count = fields.Integer(string='Договоры', compute='get_count_contract') + pol = fields.Selection(string="Пол", selection=[('m', 'Муж.'), ('j', 'Жен'), ], required=False) + type = fields.Selection(selection_add=[('director', 'Директор')]) + + def get_count_contract(self): + contract = self.env['partner.contract.customer'] + self.contract_count = contract.search_count([('partner_id', '=', self.id)]) + + def action_view_contract(self): + action = self.env.ref('contract.contract_customer_action').read()[0] + + action['domain'] = [('partner_id', '=', self.id)] + + return action + + +class ContractLine(models.Model): + _name = 'contract.line' + contract_id = fields.Many2one('partner.contract.customer', string='Order Reference', required=True, + ondelete='cascade', index=True, copy=False) + _order = "sequence desc" + # name = fields.Text(string='Название для договора') + # price_unit = fields.Float('Цена', default=0.0) + # product_uom = fields.Many2one('uom.uom', string='Единица измерения') + # product_id = fields.Many2one('product.product', string='Услуга', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict') + sequence = fields.Integer('Порядок') + name = fields.Char('Номер пункта') + punct = fields.Html('Текст пункта') + + @api.onchange('product_id') + def set_name(self): + self.name = self.product_id.name + + +class AllowedProfiles(models.Model): + _name = 'contract.allowed.profiles' + name = fields.Char(string='Одновременно включены следующие виды договоров:') + allowed_profiles = fields.Many2many('contract.profile', string='Виды договоров', required=True) + + @api.onchange('allowed_profiles') + def set_name(self): + self.name = '' + for profile in self.allowed_profiles: + self.name += profile.name + ' + ' + if self.name: + if self.name[-2] == '+': + self.name = self.name[:-2] + + +class ContractProfile(models.Model): + _name = 'contract.profile' + name = fields.Char(string='Вид договора', required=True) + payable_account_id = fields.Many2one('account.account', string='Счет кредиторской задолженности', required=True) + receivable_account_id = fields.Many2one('account.account', string='Счет дебиторской задолженности', required=True) + max_receivable_id = fields.Float(string='Максимальная деб. задолженность', required=True) + payment_term_id = fields.Many2one('account.payment.term', string='Условие оплаты', required=True) + journal_id = fields.Many2one('account.journal', string='Журнал', required=True) + + +class ContractDay(models.Model): + _name = 'contract.day' + name = fields.Char('День') diff --git a/l10n_ru_contract/models/crutch_fields_header.py b/l10n_ru_contract/models/crutch_fields_header.py new file mode 100644 index 0000000..f290eff --- /dev/null +++ b/l10n_ru_contract/models/crutch_fields_header.py @@ -0,0 +1,33 @@ +ENTITY_CONTRACT_HEADER = """ +{{object.company_id.partner_id.name}}, +именуемое в дальнейшем «Поставщик», в лице +{{(object.company_id.chief_id.partner_id.function or '').lower()}} +{{(object.name_dirprint1 or '').title()}}, + действующего на основании ОГРНИП № {{object.company_id.company_registry or ''}}, с одной стороны, и {{object.partner_id.name or ''}}, +именуемое в дальнейшем «Покупатель», в лице +{{(object.get_function_partner1(object.partner_id.id) or '').lower()}} +{{(object.name_print1 or '').title()}}, действующего на основании устава общества, с другой стороны, вместе именуемые в дальнейшем «Стороны» заключили +настоящий Договор о нижеследующем: + """ +IP_CONTACT_HEADER = """ +{{object.company_id.partner_id.name}}, +именуемое в дальнейшем «Поставщик», в лице +{{(object.company_id.chief_id.partner_id.function or '').lower()}} +{{(object.name_dirprint1 or '').title()}}, + действующего на основании ОГРНИП № {{object.company_id.company_registry or ''}}, с одной стороны, и {{object.partner_id.name or ''}}, +именуемое в дальнейшем «Покупатель», в лице +{{(object.get_function_partner1(object.partner_id.id) or '').lower()}} +{{(object.name_print1 or '').title()}}, действующего на основании ОГРНИП №{{object.partner_id.ogrn or ''}}, + с другой стороны, вместе именуемые в дальнейшем «Стороны» заключили +настоящий Договор о нижеследующем: +""" + +INDIVIDUAL_CONTRACT_HEADER = """ +{{object.company_id.partner_id.name}}, +именуемое в дальнейшем «Поставщик», в лице +{{(object.company_id.chief_id.partner_id.function or '').lower()}} +{{(object.name_dirprint1 or '').title()}}, + действующего на основании ОГРНИП № {{object.company_id.company_registry or ''}}, с одной стороны, и {{object.partner_id.name or ''}}, +именуемое в дальнейшем «Покупатель», вместе именуемые в дальнейшем «Стороны» заключили +настоящий Договор о нижеследующем: + """ diff --git a/l10n_ru_contract/models/dop_field.py b/l10n_ru_contract/models/dop_field.py new file mode 100644 index 0000000..9c8626d --- /dev/null +++ b/l10n_ru_contract/models/dop_field.py @@ -0,0 +1,353 @@ +from odoo import api, fields, models, exceptions +from datetime import datetime +import re +import pymorphy2 +from odoo.tools import pycompat + +FRACTIONS = ( + (u"десятая", u"десятых", u"десятых"), + (u"сотая", u"сотых", u"сотых"), + (u"тысячная", u"тысячных", u"тысячных"), + (u"десятитысячная", u"десятитысячных", u"десятитысячных"), + (u"стотысячная", u"стотысячных", u"стотысячных"), + (u"миллионная", u"милллионных", u"милллионных"), + (u"десятимиллионная", u"десятимилллионных", u"десятимиллионных"), + (u"стомиллионная", u"стомилллионных", u"стомиллионных"), + (u"миллиардная", u"миллиардных", u"миллиардных"), +) + +ONES = { + 0: (u"", u"", u""), + 1: (u"один", u"одна", u"одно"), + 2: (u"два", u"две", u"два"), + 3: (u"три", u"три", u"три"), + 4: (u"четыре", u"четыре", u"четыре"), + 5: (u"пять", u"пять", u"пять"), + 6: (u"шесть", u"шесть", u"шесть"), + 7: (u"семь", u"семь", u"семь"), + 8: (u"восемь", u"восемь", u"восемь"), + 9: (u"девять", u"девять", u"девять"), +} + +TENS = { + 0: u"", + 10: u"десять", + 11: u"одиннадцать", + 12: u"двенадцать", + 13: u"тринадцать", + 14: u"четырнадцать", + 15: u"пятнадцать", + 16: u"шестнадцать", + 17: u"семнадцать", + 18: u"восемнадцать", + 19: u"девятнадцать", + 2: u"двадцать", + 3: u"тридцать", + 4: u"сорок", + 5: u"пятьдесят", + 6: u"шестьдесят", + 7: u"семьдесят", + 8: u"восемьдесят", + 9: u"девяносто", +} + +HUNDREDS = { + 0: u"", + 1: u"сто", + 2: u"двести", + 3: u"триста", + 4: u"четыреста", + 5: u"пятьсот", + 6: u"шестьсот", + 7: u"семьсот", + 8: u"восемьсот", + 9: u"девятьсот", +} + +MALE = 1 +FEMALE = 2 + +import operator +import sys +import types + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + +class Partner_Bank(models.Model): + _inherit = 'res.partner.bank' + bank_corr_acc = fields.Char('Кор.счет') + + +# class Bank(models.Model): +# _inherit = 'res.bank' + +# corr_acc = fields.Char('Corresponding account', size=64) + +# class Users(models.Model): +# _inherit = 'res.users' + +# facsimile = fields.Binary("Facsimile") + +class Company(models.Model): + _inherit = 'res.company' + + inn = fields.Char(related='partner_id.inn', readonly=False) + kpp = fields.Char(related='partner_id.kpp', readonly=False) + okpo = fields.Char(related='partner_id.okpo', readonly=False) + chief_id = fields.Many2one('res.users', 'Имя директора') + stamp = fields.Binary("Stamp") + + +class Partner(models.Model): + _inherit = 'res.partner' + ogrn = fields.Char('ОГРН') + okpo = fields.Char('ОКПО') + inn = fields.Char('ИНН') + kpp = fields.Char('KPP') + passport = fields.Char('Паспорт') + + +class Report_contract_customer(models.Model): + _inherit = 'partner.contract.customer' + + def img(self, img, type='png', width=0, height=0): + if width: + width = "width='%spx'" % (width) + else: + width = " " + if height: + height = "height='%spx'" % (height) + else: + height = " " + toreturn = "" % ( + width, + height, + type, + str(pycompat.to_text(img))) + return toreturn + + def numer(self, name): + if name: + numeration = re.findall('\d+$', name) + if numeration: return numeration[0] + return '' + + def ru_date(self, date): + if date and date != 'False': + return dt.ru_strftime(u'"%d" %B %Y года', date=datetime.strptime(str(date), "%Y-%m-%d"), inflected=True) + return '' + + def ru_date2(self, date): + if date and date != 'False': + return dt.ru_strftime(u'%d %B %Y г.', date=datetime.strptime(str(date), "%Y-%m-%d %H:%M:%S"), + inflected=True) + return '' + + def in_words(self, number): + return numeral.in_words(number) + + def rubles(self, sum): + "Transform sum number in rubles to text" + text_rubles = self.numeral_rubles(int(sum)) + copeck = round((sum - int(sum)) * 100) + text_copeck = self.numeral_choose_plural(int(copeck), (u"копейка", u"копейки", u"копеек")) + return ("%s %02d %s") % (text_rubles, copeck, text_copeck) + + def numeral_rubles(self, amount, zero_for_kopeck=False): + self.check_positive(amount) + pts = [] + amount = round(amount, 2) + pts.append(self.sum_string(int(amount), 1, (u"рубль", u"рубля", u"рублей"))) + remainder = self._get_float_remainder(amount, 2) + iremainder = int(remainder) + + if iremainder != 0 or zero_for_kopeck: + if iremainder < 10 and len(remainder) == 1: + iremainder *= 10 + pts.append(self.sum_string(iremainder, 2, + (u"копейка", u"копейки", u"копеек"))) + return u" ".join(pts) + + def _get_float_remainder(self, fvalue, signs=9): + self.check_positive(fvalue) + if isinstance(fvalue, integer_types): + return "0" + if isinstance(fvalue, Decimal) and fvalue.as_tuple()[2] == 0: + return "0" + + def sum_string(self, amount, gender, items=None): + if isinstance(items, text_type): + items = split_values(items) + if items is None: + items = (u"", u"", u"") + try: + one_item, two_items, five_items = items + except ValueError: + raise ValueError("Items must be 3-element sequence") + self.check_positive(amount) + if amount == 0: + return u"ноль %s" % five_items + into = u'' + tmp_val = amount + into, tmp_val = self._sum_string_fn(into, tmp_val, gender, items) + into, tmp_val = self._sum_string_fn(into, tmp_val, FEMALE, + (u"тысяча", u"тысячи", u"тысяч")) + into, tmp_val = self._sum_string_fn(into, tmp_val, MALE, + (u"миллион", u"миллиона", u"миллионов")) + into, tmp_val = self._sum_string_fn(into, tmp_val, MALE, + (u"миллиард", u"миллиарда", u"миллиардов")) + if tmp_val == 0: + return into + else: + raise ValueError("Cannot operand with numbers bigger than 10**11") + + def _sum_string_fn(self, into, tmp_val, gender, items=None): + if items is None: + items = (u"", u"", u"") + one_item, two_items, five_items = items + self.check_positive(tmp_val) + if tmp_val == 0: + return into, tmp_val + words = [] + rest = tmp_val % 1000 + tmp_val = tmp_val // 1000 + if rest == 0: + if into == u"": + into = u"%s " % five_items + return into, tmp_val + end_word = five_items + words.append(HUNDREDS[rest // 100]) + rest = rest % 100 + rest1 = rest // 10 + tens = rest1 == 1 and TENS[rest] or TENS[rest1] + words.append(tens) + if rest1 < 1 or rest1 > 1: + amount = rest % 10 + end_word = self.numeral_choose_plural(amount, items) + words.append(ONES[amount][gender - 1]) + words.append(end_word) + words.append(into) + words = filter(lambda x: len(x) > 0, words) + return u" ".join(words).strip(), tmp_val + + def check_positive(self, value, strict=False): + if not strict and value < 0: + raise ValueError("Value must be positive or zero, not %s" % str(value)) + if strict and value <= 0: + raise ValueError("Value must be positive, not %s" % str(value)) + + def numeral_choose_plural(self, amount, variants): + if isinstance(variants, text_type): + variants = split_values(variants) + self.check_length(variants, 3) + amount = abs(amount) + if amount % 10 == 1 and amount % 100 != 11: + variant = 0 + elif amount % 10 >= 2 and amount % 10 <= 4 and \ + (amount % 100 < 10 or amount % 100 >= 20): + variant = 1 + else: + variant = 2 + return variants[variant] + + def check_length(self, value, length): + _length = len(value) + if _length != length: + raise ValueError("length must be %d, not %d" % \ + (length, _length)) + + def initials(self, fio): + if fio: + return (fio.split()[0] + ' ' + ''.join([fio[0:1] + '.' for fio in fio.split()[1:]])).strip() + return '' + + def address(self, partner): + repr = [] + if partner.zip: repr.append(partner.zip) + if partner.city: repr.append(partner.city) + if partner.street: repr.append(partner.street) + if partner.street2: repr.append(partner.street2) + return ', '.join(repr) + + def address_delivery(self, partner): + if partner: + addr = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'delivery')], limit=1) + repr = [] + if addr: + if addr.zip: repr.append(addr.zip) + if addr.city: repr.append(addr.city) + if addr.street: repr.append(addr.street) + if addr.street2: repr.append(addr.street2) + return ', '.join(repr) + + def get_function_print(self, function): + morph = pymorphy2.MorphAnalyzer() + if function: + f = morph.parse(function)[0] + f = f.inflect({'gent'}).word + return f.title() + + def get_function_partnerip(self, partner): + director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) + if director: + if director.function: + return director.function + + def get_function_partner(self, partner): + res = [] + morph = pymorphy2.MorphAnalyzer() + if partner: + director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) + if director: + if director.function: + list_f = str(director.function).split(' ') + for func in list_f: + f = morph.parse(func)[0] + f = f.inflect({'gent'}).word + res.append(f) + return ' '.join(res) + + def get_function_partner1(self, partner): + if partner: + director = self.env['res.partner'].search([('parent_id', '=', partner), ('type', '=', 'director')], limit=1) + if director: + if director.function: + return director.function + + def get_date_text(self, date): + month_list = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', + 'ноября', 'декабря'] + if date: + date_list = str(date).split('-') + if date_list[0] and date_list[1] and date_list[2]: + return ('"' + date_list[2] + '" ' + month_list[int(date_list[1]) - 1] + ' ' + date_list[0] + ' г.') + + def get_bank(self, partner): + repr = [] + bank = None + if partner.bank_ids: + bank = partner.bank_ids[0] + elif partner.parent_id.bank_ids: + bank = partner.parent_id.bank_ids[0] + if bank and bank.bank_name: repr.append(bank.bank_name) + if bank and bank.acc_number: repr.append(u"Р/счет " + bank.acc_number) + if bank and bank.bank_bic: repr.append(u"БИК " + bank.bank_bic) + if bank and bank.bank_corr_acc: repr.append(u"к/с " + bank.bank_corr_acc) + return '
'.join(repr) diff --git a/l10n_ru_contract/models/invoice_saleorder.py b/l10n_ru_contract/models/invoice_saleorder.py new file mode 100644 index 0000000..3bfa5bd --- /dev/null +++ b/l10n_ru_contract/models/invoice_saleorder.py @@ -0,0 +1,128 @@ +from odoo import api, fields, models, exceptions +from datetime import datetime + + +class contract_sale_order(models.Model): + _inherit = 'sale.order' + mt_contractid = fields.Many2one('partner.contract.customer', string='Номер договора') + sec_partner_id = fields.Many2one('res.partner', string='Контрагент', store=True, compute='get_pid') + stamp = fields.Boolean(string='Печать и подпись', related='mt_contractid.stamp') + + @api.depends('partner_id') + def get_pid(self): + for s in self: + s.sec_partner_id = s.partner_id.parent_id if s.partner_id.parent_id else s.partner_id + + @api.onchange('mt_contractid') + def set_ons(self): + if self.mt_contractid: + self.payment_term_id = self.mt_contractid.payment_term_id + + @api.constrains('state') + def late_payment_check(self): + if self.mt_contractid: + if self.state == 'sale': + late_invoices_count = 0 + max_receivable = self.mt_contractid.profile_id.max_receivable_id # макс. деб. задолженность в договоре + # ищу просроченные инвойсы контрагента указанного в заказе со стейтом "Подтверждено" + invoices_obj = self.env['account.move'].search([('partner_id', '=', self.partner_id.id), + ('state', '=', 'posted'), + ('invoice_date_due', '<', datetime.now().date())]) + + for invoice in invoices_obj: + late_invoices_count += invoice.amount_residual # складываю деб. задолженность по просроченным инвойсам + + if late_invoices_count > max_receivable: + raise exceptions.ValidationError( + f'Нельзя подтвердить заказ, так как у контрагента {self.sec_partner_id.name} нарушено ' + f'условие по дебиторской задолженности.\n\n' + f'Контрагент {self.sec_partner_id.name} должен {late_invoices_count}руб.\n' + f'Максимальная дебиторская задолженность указанная в ' + f'договоре №{self.mt_contractid.name} - {max_receivable}руб.\n\n' + f'Проверьте следующие неоплаченные счета контрагента:\n' + f'{", ".join([invoice.name for invoice in invoices_obj])}') + + # при выбора счета "Обычный счет" + @api.model + def _create_invoices(self, grouped=False, final=False, date=None): + res = super(contract_sale_order, self)._create_invoices(grouped, final, date) + if self.mt_contractid: + res.write({'mt_contractid': self.mt_contractid, + 'journal_id': self.mt_contractid.profile_id.journal_id}) + # 'invoice_payment_term_id': self.mt_contractid.profile_id.payment_term_id + # 'line_ids': [(0, 0, { + # 'account_id': self.mt_contractid.profile_id.receivable_account_id.id})] + return res + + +class ContractCreateInvoice(models.TransientModel): + _inherit = 'sale.advance.payment.inv' + + # при выбора счета "Авансовый платеж" + @api.model + def _create_invoice(self, order, so_line, amount): + res = super(ContractCreateInvoice, self)._create_invoice(order, so_line, amount) + if order.mt_contractid: + res.write({'mt_contractid': order.mt_contractid, + 'journal_id': order.mt_contractid.profile_id.journal_id, }) + # 'invoice_payment_term_id': order.mt_contractid.profile_id.payment_term_id + return res + + +class contract_invoice(models.Model): + _inherit = 'account.move' + mt_contractid = fields.Many2one('partner.contract.customer', string='Номер договора') + sf_number = fields.Char(string='Номер с/ф') + osnovanie = fields.Char(string='Основание') + sec_partner_id = fields.Many2one('res.partner', string='Контрагент', store=True, compute='get_pid') + stamp = fields.Boolean(string='Печать и подпись', related='mt_contractid.stamp') + + @api.depends('partner_id') + def get_pid(self): + for s in self: + s.sec_partner_id = s.partner_id.parent_id if s.partner_id.parent_id else s.partner_id + + @api.onchange('mt_contractid') + def set_ons(self): + if self.mt_contractid: + self.osnovanie = 'Договор № ' + self.mt_contractid.name + ' от ' + fields.Datetime.from_string( + self.mt_contractid.date_start).strftime("%d.%m.%Y") + + @api.constrains('state') + def invoice_fields_check(self): + for s in self: + if s.state == 'posted': + if s.mt_contractid: + errors_list = [] + journal_in_contract = s.mt_contractid.profile_id.journal_id + payment_term_in_contract = s.mt_contractid.profile_id.payment_term_id + receivable_in_contract = s.mt_contractid.profile_id.receivable_account_id + + if journal_in_contract != s.journal_id: + errors_list.append(f'Отличается Журнал - [{s.journal_id.name}] ' + f'и указанный в договоре №{s.mt_contractid.name} ' + f'Журнал - [{journal_in_contract.name}]\n\n') + + if payment_term_in_contract != s.invoice_payment_term_id: + errors_list.append(f'Отличается поле "Условие оплаты" в инвойсе ' + f'[Условие оплаты - {s.invoice_payment_term_id.name}] ' + f'и указанный в договоре №{s.mt_contractid.name} ' + f'[Условие оплаты - {payment_term_in_contract.name}]\n\n') + + if receivable_in_contract not in s.line_ids.account_id: + errors_list.append(f'Отличается поле "Счет дебиторской задолженности" в инвойсе ' + f'и указанный в договоре №{s.mt_contractid.name}') + + if errors_list: + raise exceptions.ValidationError(''.join(errors_list)) + + +class contact_purchase_order(models.Model): + _inherit = 'purchase.order' + mt_contractid = fields.Many2one('partner.contract.customer', string='Номер договора') + sec_partner_id = fields.Many2one('res.partner', string='Контрагент', store=True, compute='get_pid') + + @api.depends('partner_id') + def get_pid(self): + for s in self: + s.sec_partner_id = s.partner_id.parent_id if s.partner_id.parent_id else s.partner_id diff --git a/l10n_ru_contract/report/__init__.py b/l10n_ru_contract/report/__init__.py new file mode 100644 index 0000000..6527450 --- /dev/null +++ b/l10n_ru_contract/report/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import report_contract,report_contract_order,report_contract_invoce diff --git a/l10n_ru_contract/report/report_contract.py b/l10n_ru_contract/report/report_contract.py new file mode 100644 index 0000000..3baf414 --- /dev/null +++ b/l10n_ru_contract/report/report_contract.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from odoo import api, models + +class ContractCustomerReport(models.AbstractModel): + _name = 'contract.customer.report' + + + def get_report_values(self, docids, data=None): + docs = self.env['partner.contract.customer'].browse(docids) + return { + 'doc_ids': docs.ids, + 'doc_model': 'partner.contract.customer', + 'docs': docs, + } diff --git a/l10n_ru_contract/report/report_contract.xml b/l10n_ru_contract/report/report_contract.xml new file mode 100644 index 0000000..361871d --- /dev/null +++ b/l10n_ru_contract/report/report_contract.xml @@ -0,0 +1,398 @@ + + + + + + A4 + + A4 + 0 + 0 + Portrait + 15 + 30 + 7 + 7 + + 10 + 90 + + + + Договор + partner.contract.customer + qweb-pdf + contract.report_contract_customer + contract.report_contract_customer + 'Договор - %s' % (object.name) + + + report + + diff --git a/l10n_ru_contract/report/report_contract_invoce.py b/l10n_ru_contract/report/report_contract_invoce.py new file mode 100644 index 0000000..f07f090 --- /dev/null +++ b/l10n_ru_contract/report/report_contract_invoce.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from odoo import api, models + + +class ContractCustomerReportOrder(models.AbstractModel): + _name = 'contract.customer.report_invoce' + + + def get_report_values(self, docids, data=None): + docs = self.env['account.move'].browse(docids) + return { + 'doc_ids': docs.ids, + 'doc_model': 'account.move', + 'docs': docs, + } diff --git a/l10n_ru_contract/report/report_contract_invoce.xml b/l10n_ru_contract/report/report_contract_invoce.xml new file mode 100644 index 0000000..634df7f --- /dev/null +++ b/l10n_ru_contract/report/report_contract_invoce.xml @@ -0,0 +1,592 @@ + + + + + + A4 + + A4 + 0 + 0 + Portrait + 15 + 30 + 7 + 7 + + 10 + 90 + + + + Договор со спецификацией + account.move + qweb-pdf + contract.report_contract_customer_invoce + contract.report_contract_customer_invoce + 'Договор со спецификацией - %s' % (object.name) + + + report + + diff --git a/l10n_ru_contract/report/report_contract_order.py b/l10n_ru_contract/report/report_contract_order.py new file mode 100644 index 0000000..42a679d --- /dev/null +++ b/l10n_ru_contract/report/report_contract_order.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from odoo import api, models + + +class ContractCustomerReportOrder(models.AbstractModel): + _name = 'contract.customer.report_order' + + + def get_report_values(self, docids, data=None): + docs = self.env['sale.order'].browse(docids) + return { + 'doc_ids': docs.ids, + 'doc_model': 'sale.order', + 'docs': docs, + } + + + diff --git a/l10n_ru_contract/report/report_contract_order.xml b/l10n_ru_contract/report/report_contract_order.xml new file mode 100644 index 0000000..4e03297 --- /dev/null +++ b/l10n_ru_contract/report/report_contract_order.xml @@ -0,0 +1,594 @@ + + + + + + A4 + + A4 + 0 + 0 + Portrait + 15 + 30 + 7 + 7 + + 10 + 90 + + + + Договор со спецификацией + sale.order + qweb-pdf + contract.report_contract_customer_order + contract.report_contract_customer_order + 'Договор со спецификацией - %s' % (object.name) + + + report + + diff --git a/l10n_ru_contract/report/report_contract_order1.xml b/l10n_ru_contract/report/report_contract_order1.xml new file mode 100644 index 0000000..2b8a887 --- /dev/null +++ b/l10n_ru_contract/report/report_contract_order1.xml @@ -0,0 +1,406 @@ + + + + + + A4 + + A4 + 0 + 0 + Portrait + 15 + 30 + 7 + 7 + + 10 + 90 + + + + Спецификация + sale.order + qweb-pdf + contract.report_contract_customer_order1 + contract.report_contract_customer_order1 + 'Спецификация - %s' % (object.name) + + + report + + diff --git a/l10n_ru_contract/security/ir.model.access.csv b/l10n_ru_contract/security/ir.model.access.csv new file mode 100644 index 0000000..4f09569 --- /dev/null +++ b/l10n_ru_contract/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_partner_contract_customer,access_partner_contract_customer,model_partner_contract_customer,,1,1,1,1 +access_contract_profile,access_contract_profile,model_contract_profile,,1,1,1,1 +access_contract_day,access_contract_day,model_contract_day,,1,1,1,1 +access_contract_allowed_profiles,access_contract_allowed_profiles,model_contract_allowed_profiles,,1,1,1,1 +access_partner_contract_customerline,access_partner_contract_customerline,model_contract_line,,1,1,1,1 diff --git a/l10n_ru_contract/views/contract_customer_view.xml b/l10n_ru_contract/views/contract_customer_view.xml new file mode 100644 index 0000000..ca1ecd9 --- /dev/null +++ b/l10n_ru_contract/views/contract_customer_view.xml @@ -0,0 +1,358 @@ + + + + + + Договор + partner.contract.customer + +
+
+ +
+ +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + view_saleorder_form + sale.order + + + + + + + + + + + + + view_purchaseorder_formcontr + purchase.order + + + + + + + + + + + + + + view_invoice_form + account.move + + + + + + + + + + + + + + + + Договор + partner.contract.customer + + + + + + + + + + +
+
+
+ Номер: +
+
+ Контрагент:
+ Тип:
+ Вид договора: +
+
+ Наша компания: +
+
+
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+
diff --git a/l10n_ru_doc/__init__.py b/l10n_ru_doc/__init__.py new file mode 100644 index 0000000..10a94a5 --- /dev/null +++ b/l10n_ru_doc/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import report_helper +from . import report diff --git a/l10n_ru_doc/__manifest__.py b/l10n_ru_doc/__manifest__.py new file mode 100644 index 0000000..49537f1 --- /dev/null +++ b/l10n_ru_doc/__manifest__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Russia - Documents", + + 'summary': "Первичные документы", + + 'description': """ +The module for print documents in accordance laws of Russia. +============================================================ +Возможности: + * Товарная накладная (ТОРГ-12) + * Счет на оплату + * Счет-фактура + * Акт выполненных работ + * Вывод подписей и печати + """, + + 'author': "CodUP and MKLab", + 'website': "https://inf-centre.ru", + + 'license': 'AGPL-3', + 'category': 'Localization', + 'version': '17.0.2024.06.28', + + 'depends': ['base','sale','account','sale_stock','uom'], + + 'external_dependencies': {'python' : ['pytils']}, + + 'data': [ + 'views/account_invoice_view.xml', + 'views/res_partner_view.xml', + 'views/res_company_view.xml', + 'views/res_users_view.xml', + 'views/res_bank_view.xml', + 'views/uom.xml', + 'views/tax.xml', + 'views/product.xml', + 'views/l10n_ru_doc_data.xml', + 'report/l10n_ru_doc_report.xml', + 'report/report_order.xml', + 'report/report_invoice.xml', + 'report/report_bill.xml', + 'report/report_act.xml', + 'report/report_upd.xml', + 'report/report_updn.xml', + ], + + 'demo': [ + 'demo/l10n_ru_doc_demo.xml', + ], +} diff --git a/l10n_ru_doc/__pycache__/__init__.cpython-310.pyc b/l10n_ru_doc/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..59706c9 Binary files /dev/null and b/l10n_ru_doc/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_doc/__pycache__/report_helper.cpython-310.pyc b/l10n_ru_doc/__pycache__/report_helper.cpython-310.pyc new file mode 100644 index 0000000..371ad1e Binary files /dev/null and b/l10n_ru_doc/__pycache__/report_helper.cpython-310.pyc differ diff --git a/l10n_ru_doc/demo/l10n_ru_doc_demo.xml b/l10n_ru_doc/demo/l10n_ru_doc_demo.xml new file mode 100644 index 0000000..16d7d63 --- /dev/null +++ b/l10n_ru_doc/demo/l10n_ru_doc_demo.xml @@ -0,0 +1,63 @@ + + + + + + ОАО "СБЕРБАНК РОССИИ" + г.Москва + 044525225 + 30101810400000000225 + + + + 40707810600025341231 + + + + + + + iVBORw0KGgoAAAANSUhEUgAAALQAAABACAYAAACzzl09AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2lpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoxMGE3N2JhNy01MThlLTMwNGEtODcxOC0wMmQ5MWYzYTdiNTUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MjQ2QTI3QjdCQ0JCMTFFMzgwRTQ4NjcwRjlEM0QyMTAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjQ2QTI3QjZCQ0JCMTFFMzgwRTQ4NjcwRjlEM0QyMTAiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkYzOTcwMjU2QkNCOTExRTM5OEZFRDU5OUQxQUU3MUUzIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkYzOTcwMjU3QkNCOTExRTM5OEZFRDU5OUQxQUU3MUUzIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+LLXcsQAAAjFJREFUeNrs3TFKM0EUB3BXv+QAgqViYWflBQLbWNhZKwE7QQSPkCMIItgJQW+gtjmCBxLZr34zsMPDiCK/X6c7swPxz/B4zJhuGIYN+Cs2fQQINAg0fL9/5S+6o6NUUT28v3dh/s1NnP/5OT7/7i7Ov7qK83d24vjFoht7X/f4GOfv78f5fd/5s9uhQaBBoGGdNXSzRs7W2GWNfH09On+4v4/jF4vcehcXcf5qpdFuhwaBBoEGgQaBRqBBoOHnNPvQ2b7zxtZWnF+e7SieV+uVZzkOD0efV33r8ixH38f5RV/a2Q47NAg0CDQkde4UYocGgQaBhpT6TuF8Hovq7e3w43B7O36nr+9z55dXq9hHXi7j/N3dOL7RN+6Oj8fXn07j+15e9KHt0CDQINDwpRp6WC5TdwDTNXKjxh7m8zB+mr0TOJnE972+xvVPTzXe7dAg0CDQsNYa+rf58DfCDo1Ag0DDL6+hu7Oz2Kc9OIjPi7MeZd+6/H/Q3WyW6vtWZzlms/j86Smuf34e1/+IVXd1tqM4y4EdGgQaBBqS3CnEDg0CDQINOXUf+u0tdyfw5CSeN354iPP39kbHV+snz19X3+FS9tHLO5HFeOzQINAg0PDVGrpZIzdq7OHyMjU+XSO3vufw+XmtdyKxQ4NAg0BDq4aeNCZMfGbYoUGgQaBRQ4+aJvvI1VmO8k5g8bzsW1fvS/aRm3cii/c522GHBoEGgYYkdwqxQ4NAg0BDzn8BBgBlrqW6k1gn5wAAAABJRU5ErkJggg== + codup-test@mail.ru + www.codup.com + 0000000000 + 000000000 + 00000000 + + + + ООО "CodUP" + ул.Земляничная + дом № 13 + 123456 + г.Москва + CodUP + + + 1 + 1 + iVBORw0KGgoAAAANSUhEUgAAAOYAAADmCAMAAAD2tAmJAAAC2VBMVEUWAGH///8WAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGEWAGF7Mdv3AAAA83RSTlMAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0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 + + + + Иванов Иван Петрович + codup-test@mail.ru + + + + Иванов Иван Петрович + iVBORw0KGgoAAAANSUhEUgAAAZQAAAETCAMAAAAmrgDyAAAC61BMVEUOF9r///8OF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9oOF9r/ATCgAAAA+HRSTlMAAAECAwQFBgcICQoLDA0ODxAREhQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJj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 + + + + Сидоров Семен Иванович + codup-test@mail.ru + + + + Сидоров Семен Иванович + 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 + + + + diff --git a/l10n_ru_doc/i18n/ru.po b/l10n_ru_doc/i18n/ru.po new file mode 100644 index 0000000..59d03de --- /dev/null +++ b/l10n_ru_doc/i18n/ru.po @@ -0,0 +1,116 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_ru_doc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-08-19 09:27+0000\n" +"PO-Revision-Date: 2020-08-19 09:27+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_ru_doc +#: model:ir.actions.report,name:l10n_ru_doc.report_account_invoice_act +msgid "Act" +msgstr "Акт об оказании услуг" + +#. module: l10n_ru_doc +#: model:ir.actions.report,name:l10n_ru_doc.report_account_invoice_bill +msgid "Bill" +msgstr "Товарная накладная (ТОРГ-12)" + +#. module: l10n_ru_doc +#: model:ir.model.fields,help:l10n_ru_doc.field_res_company__print_facsimile +#: model:ir.model.fields,help:l10n_ru_doc.field_res_users__print_facsimile +msgid "Check this for adding Facsimiles of responsible persons to documents." +msgstr "Отметьте, для вставки подписей ответственных лиц в документы." + +#. module: l10n_ru_doc +#: model:ir.model.fields,help:l10n_ru_doc.field_res_company__print_stamp +msgid "Check this for adding Stamp of company to documents." +msgstr "Отметьте, для вставки печати организации в документы." + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__chief_id +msgid "Chief" +msgstr "Руководитель организации" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_account_setup_bank_manual_config__bank_corr_acc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_bank__corr_acc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_partner_bank__bank_corr_acc +msgid "Corresponding account" +msgstr "Корр. счет" + +#. module: l10n_ru_doc +#: model_terms:ir.ui.view,arch_db:l10n_ru_doc.view_company_ru_form +msgid "Documents" +msgstr "Документы" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_users__facsimile +msgid "Facsimile" +msgstr "Подпись" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__inn +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_partner__inn +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_users__inn +msgid "INN" +msgstr "ИНН" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__kpp +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_partner__kpp +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_users__kpp +msgid "KPP" +msgstr "КПП" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__okpo +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_partner__okpo +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_users__okpo +msgid "OKPO" +msgstr "ОКПО" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__print_anywhere +msgid "Print Anywhere" +msgstr "Документы" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__print_facsimile +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_users__print_facsimile +msgid "Print Facsimile" +msgstr "Выводить подписи" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__print_stamp +msgid "Print Stamp" +msgstr "Выводить печать" + +#. module: l10n_ru_doc +#: model_terms:ir.ui.view,arch_db:l10n_ru_doc.view_company_ru_form +msgid "Responsible Persons" +msgstr "Ответственные лица" + +#. module: l10n_ru_doc +#: model:ir.model.fields,field_description:l10n_ru_doc.field_res_company__stamp +msgid "Stamp" +msgstr "Печать" + +#. module: l10n_ru_doc +#: model:ir.model.fields,help:l10n_ru_doc.field_res_company__print_anywhere +msgid "Uncheck this, if you want add Facsimile and Stamp only in email." +msgstr "Снимите отметку, если хотите добавлять подписи и печать только в email." + +#. module: l10n_ru_doc +#: model:ir.actions.report,name:l10n_ru_doc.report_account_invoice_upd +msgid "Upd" +msgstr "Универсальный платежный документ (УПД)" diff --git a/l10n_ru_doc/models/__init__.py b/l10n_ru_doc/models/__init__.py new file mode 100644 index 0000000..d378530 --- /dev/null +++ b/l10n_ru_doc/models/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from . import res_partner +from . import res_company +from . import res_users +from . import res_bank +from . import account_invoice +from . import account_move_line +from . import sale +from . import uom +from . import tax +from . import product diff --git a/l10n_ru_doc/models/__pycache__/__init__.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3d51288 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/account_invoice.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/account_invoice.cpython-310.pyc new file mode 100644 index 0000000..9900076 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/account_invoice.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/account_move_line.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/account_move_line.cpython-310.pyc new file mode 100644 index 0000000..2bdf1b4 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/account_move_line.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/product.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/product.cpython-310.pyc new file mode 100644 index 0000000..2e4d98f Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/product.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/res_bank.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/res_bank.cpython-310.pyc new file mode 100644 index 0000000..d73d6d5 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/res_bank.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/res_company.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/res_company.cpython-310.pyc new file mode 100644 index 0000000..aaaff18 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/res_company.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/res_partner.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/res_partner.cpython-310.pyc new file mode 100644 index 0000000..ed7504e Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/res_partner.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/res_users.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/res_users.cpython-310.pyc new file mode 100644 index 0000000..b0c10a5 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/res_users.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/sale.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/sale.cpython-310.pyc new file mode 100644 index 0000000..94725f8 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/sale.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/tax.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/tax.cpython-310.pyc new file mode 100644 index 0000000..c251382 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/tax.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/__pycache__/uom.cpython-310.pyc b/l10n_ru_doc/models/__pycache__/uom.cpython-310.pyc new file mode 100644 index 0000000..3399650 Binary files /dev/null and b/l10n_ru_doc/models/__pycache__/uom.cpython-310.pyc differ diff --git a/l10n_ru_doc/models/account_invoice.py b/l10n_ru_doc/models/account_invoice.py new file mode 100644 index 0000000..bf5c88a --- /dev/null +++ b/l10n_ru_doc/models/account_invoice.py @@ -0,0 +1,144 @@ +from datetime import datetime +from odoo import api, fields, models + +class AccountInvoice(models.Model): + _inherit = 'account.move' + kladov=fields.Many2one('res.users', string='Ответственный за передачу товаров/услуг') + gruzopol=fields.Many2one('res.partner', string='Грузополучатель') + gruzootpr=fields.Many2one('res.partner', string='Грузоотправитель') + transport=fields.Char('Данные о транспортировке и грузе') + osnovanie=fields.Char('Основание') + payment_text=fields.Char('Текст для платежек в УПД', compute='_compute_get_txtpayment') + payment_num=fields.Char('Номер платежки в УПД', compute='_compute_get_txtpayment') + payment_date = fields.Char('Дата платежки в УПД', compute='_compute_get_txtpayment') + only_service = fields.Boolean('Только услуги', compute='_compute_get_check_service') + + @api.depends('invoice_line_ids') + def _compute_get_check_service(self): + for s in self: + s.only_service = all((line.product_id.type=='service') for line in s.invoice_line_ids) + + def _compute_get_txtpayment(self): + for s in self: + payments = s._get_reconciled_payments() + payment_text = '' + + for payment in payments: + if payment.date: + payment_text += payment.name + ' от ' + \ + fields.Datetime.from_string(payment.date).strftime("%d.%m.%Y") + if payments[-1]!=payment: + payment_text += ', ' + if payments: + s.payment_num = payments[0].name + s.payment_date = fields.Datetime.from_string(payments[0].date).strftime("%d.%m.%Y") + else: + s.payment_num = '' + s.payment_text = '' + s.payment_date = '' + + s.payment_text = payment_text + + def action_bill_sent(self): + assert len(self) == 1, 'This option should only be used for a single id at a time.' + template = self.env.ref('account.email_template_edi_invoice', False) + compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) + ctx = { + 'default_model': 'account.move', + 'default_res_id': self.id, + 'default_use_template': bool(template), + 'default_template_id': template.id, + 'default_composition_mode': 'comment', + 'mark_invoice_as_sent': True, + } + return { + 'name': 'Compose Email', + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(compose_form.id, 'form')], + 'view_id': compose_form.id, + 'target': 'new', + 'context': ctx, + } + + def bill_print(self): + assert len(self) == 1, 'This option should only be used for a single id at a time.' + return self.env['report'].get_action(self, 'l10n_ru_doc.report_upd') + + def get_delivery_doc_name(self): + for s in self: + pickings = [] + pickings_list = '0' + orders = self.env['sale.order'].sudo().search([('name','=',s.invoice_origin)]) + for o in orders: + if o.picking_ids: + for p in o.picking_ids: + pickings.append(p.name) + if len(pickings)>0: + pickings_list = ';'.join(pickings) + if pickings_list != '0': + return pickings_list + if s.name.find('/') > -1: + return 'УПД № ' + s.name[len(s.name) - 4:] + return 'УПД № ' + s.name + + def get_delivery_doc_date(self): + for s in self: + pickings = [] + pickings_list = '0' + orders = self.env['sale.order'].sudo().search([('name','=',s.invoice_origin)]) + for o in orders: + if o.picking_ids: + for p in o.picking_ids: + pickings.append(datetime.strftime(p.date, '%d.%m.%Y')) + if len(pickings)>0: + pickings_list = ';'.join(pickings) + if pickings_list != '0': + return pickings_list + return datetime.strftime(s.date, '%d.%m.%Y') + + def get_function_partner(self, partner=False, type='director'): + if partner: + if partner.parent_id: + partner = partner.parent_id + director = self.env['res.partner'].search([('parent_id', '=', partner.id), + ('type', '=', type)], limit=1) + if director: + if director.function: + return director.function + return '' + + def get_name_partner(self, partner=False, type='director'): + if partner: + if partner.parent_id: + partner = partner.parent_id + director = self.env['res.partner'].search([('parent_id', '=', partner.id), + ('type', '=', type)], limit=1) + if director: + if director.name: + return director.name + return '' + + def get_facsimile_partner(self, partner=False, type='director'): + if partner: + if partner.parent_id: + partner = partner.parent_id + director = self.env['res.partner'].search([('parent_id', '=', partner.id), + ('type', '=', type)], + limit=1) + if director: + if director.facsimile: + return director.facsimile + return '' + + def get_stamp_partner(self, partner=False): + if partner: + if partner.parent_id: + partner = partner.parent_id + if partner.stamp: + return partner.stamp + return False + + diff --git a/l10n_ru_doc/models/account_move_line.py b/l10n_ru_doc/models/account_move_line.py new file mode 100644 index 0000000..06e0f5e --- /dev/null +++ b/l10n_ru_doc/models/account_move_line.py @@ -0,0 +1,29 @@ +from datetime import datetime +from odoo import api, fields, models + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + price_total_pf = fields.Monetary( + string='TotalPF', + compute='_compute_totals', + currency_field='currency_id', + ) + + @api.depends('quantity', 'discount', 'price_unit', 'tax_ids', 'currency_id') + def _compute_totals(self): + super(AccountMoveLine,self)._compute_totals() + for line in self: + line_discount_price_unit = line.price_unit * (1 - (line.discount / 100.0)) + if line.tax_ids.filtered(lambda tax: tax.invisiblePF == False): + taxes_res = line.tax_ids.filtered(lambda tax: tax.invisiblePF == False).compute_all( + line_discount_price_unit, + quantity=line.quantity, + currency=line.currency_id, + product=line.product_id, + partner=line.partner_id, + is_refund=line.is_refund, + ) + line.price_total_pf = taxes_res['total_included'] + else: + line.price_total_pf = line.price_total \ No newline at end of file diff --git a/l10n_ru_doc/models/product.py b/l10n_ru_doc/models/product.py new file mode 100644 index 0000000..c230841 --- /dev/null +++ b/l10n_ru_doc/models/product.py @@ -0,0 +1,5 @@ +from odoo import fields, models + +class ProductTnved(models.Model): + _inherit = 'product.product' + kod_tnved = fields.Char('Код ТНВЭД') diff --git a/l10n_ru_doc/models/res_bank.py b/l10n_ru_doc/models/res_bank.py new file mode 100644 index 0000000..f22d430 --- /dev/null +++ b/l10n_ru_doc/models/res_bank.py @@ -0,0 +1,19 @@ +from odoo import api, fields, models + +class Bank(models.Model): + _inherit = 'res.bank' + + corr_acc = fields.Char('Corresponding account', size=64) + + +class ResPartnerBank(models.Model): + _inherit = 'res.partner.bank' + + bank_corr_acc = fields.Char('Corresponding account', size=64) + + @api.onchange('bank_id') + def onchange_bank_id(self): + for s in self: + s.bank_name = s.bank_id.name + s.bank_bic = s.bank_id.bic + s.bank_corr_acc = s.bank_id.corr_acc diff --git a/l10n_ru_doc/models/res_company.py b/l10n_ru_doc/models/res_company.py new file mode 100644 index 0000000..9ca4ae7 --- /dev/null +++ b/l10n_ru_doc/models/res_company.py @@ -0,0 +1,18 @@ +from odoo import fields, models + +class Company(models.Model): + _inherit = 'res.company' + + inn = fields.Char(related='partner_id.inn', readonly=False) + kpp = fields.Char(related='partner_id.kpp', readonly=False) + okpo = fields.Char(related='partner_id.okpo', readonly=False) + chief_id = fields.Many2one('res.users', 'Chief') + accountant_id = fields.Many2one('res.users', 'General Accountant') + print_facsimile = fields.Boolean(string='Print Facsimile', + help="Check this for adding Facsimiles of responsible persons to documents.") + print_stamp = fields.Boolean(string='Print Stamp', + help="Check this for adding Stamp of company to documents.") + stamp = fields.Binary("Stamp") + print_anywhere = fields.Boolean(string='Print Anywhere', + help="Uncheck this, if you want add Facsimile and Stamp only in email.", + default=True) diff --git a/l10n_ru_doc/models/res_partner.py b/l10n_ru_doc/models/res_partner.py new file mode 100644 index 0000000..e918a39 --- /dev/null +++ b/l10n_ru_doc/models/res_partner.py @@ -0,0 +1,11 @@ +from odoo import fields, models +class ResPartner(models.Model): + _inherit = 'res.partner' + + inn = fields.Char('INN', related='vat') + kpp = fields.Char('KPP', size=9) + okpo = fields.Char('OKPO', size=14) + ogrn = fields.Char('ОГРН') + type = fields.Selection(selection_add=[('director', 'Директор'), ('accountant', 'Бухгалтер')]) + facsimile = fields.Binary("Подпись") + stamp = fields.Binary("Печать") diff --git a/l10n_ru_doc/models/res_users.py b/l10n_ru_doc/models/res_users.py new file mode 100644 index 0000000..aaf70cb --- /dev/null +++ b/l10n_ru_doc/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + +class Users(models.Model): + _inherit = 'res.users' + + print_facsimile = fields.Boolean(related='company_id.print_facsimile') + facsimile = fields.Binary() diff --git a/l10n_ru_doc/models/sale.py b/l10n_ru_doc/models/sale.py new file mode 100644 index 0000000..54707e2 --- /dev/null +++ b/l10n_ru_doc/models/sale.py @@ -0,0 +1,8 @@ +from odoo import models + +class SaleOrder(models.Model): + _inherit = 'sale.order' + def print_quotation(self): + self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'}) + return self.env['report'].get_action(self, 'l10n_ru_doc.report_order') + diff --git a/l10n_ru_doc/models/tax.py b/l10n_ru_doc/models/tax.py new file mode 100644 index 0000000..081adb1 --- /dev/null +++ b/l10n_ru_doc/models/tax.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class TaxInherit(models.Model): + _inherit = 'account.tax' + + invisiblePF = fields.Boolean('Не видно в ПФ') diff --git a/l10n_ru_doc/models/uom.py b/l10n_ru_doc/models/uom.py new file mode 100644 index 0000000..1de1e3b --- /dev/null +++ b/l10n_ru_doc/models/uom.py @@ -0,0 +1,4 @@ +from odoo import fields, models +class UomInherit(models.Model): + _inherit = 'uom.uom' + kod = fields.Char('Код единицы измерения') diff --git a/l10n_ru_doc/report/__init__.py b/l10n_ru_doc/report/__init__.py new file mode 100644 index 0000000..b058f02 --- /dev/null +++ b/l10n_ru_doc/report/__init__.py @@ -0,0 +1,5 @@ +from . import report_order +from . import report_invoice +from . import report_bill +from . import report_act +from . import report_upd diff --git a/l10n_ru_doc/report/__pycache__/__init__.cpython-310.pyc b/l10n_ru_doc/report/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..af59652 Binary files /dev/null and b/l10n_ru_doc/report/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_doc/report/__pycache__/report_act.cpython-310.pyc b/l10n_ru_doc/report/__pycache__/report_act.cpython-310.pyc new file mode 100644 index 0000000..e8bc325 Binary files /dev/null and b/l10n_ru_doc/report/__pycache__/report_act.cpython-310.pyc differ diff --git a/l10n_ru_doc/report/__pycache__/report_bill.cpython-310.pyc b/l10n_ru_doc/report/__pycache__/report_bill.cpython-310.pyc new file mode 100644 index 0000000..dd10562 Binary files /dev/null and b/l10n_ru_doc/report/__pycache__/report_bill.cpython-310.pyc differ diff --git a/l10n_ru_doc/report/__pycache__/report_invoice.cpython-310.pyc b/l10n_ru_doc/report/__pycache__/report_invoice.cpython-310.pyc new file mode 100644 index 0000000..7818d1a Binary files /dev/null and b/l10n_ru_doc/report/__pycache__/report_invoice.cpython-310.pyc differ diff --git a/l10n_ru_doc/report/__pycache__/report_order.cpython-310.pyc b/l10n_ru_doc/report/__pycache__/report_order.cpython-310.pyc new file mode 100644 index 0000000..ab3e710 Binary files /dev/null and b/l10n_ru_doc/report/__pycache__/report_order.cpython-310.pyc differ diff --git a/l10n_ru_doc/report/__pycache__/report_upd.cpython-310.pyc b/l10n_ru_doc/report/__pycache__/report_upd.cpython-310.pyc new file mode 100644 index 0000000..619c778 Binary files /dev/null and b/l10n_ru_doc/report/__pycache__/report_upd.cpython-310.pyc differ diff --git a/l10n_ru_doc/report/l10n_ru_doc_report.xml b/l10n_ru_doc/report/l10n_ru_doc_report.xml new file mode 100644 index 0000000..328441b --- /dev/null +++ b/l10n_ru_doc/report/l10n_ru_doc_report.xml @@ -0,0 +1,107 @@ + + + + + A4 + + A4 + 0 + 0 + Portrait + 7 + 7 + 7 + 7 + + 35 + 75 + + + A4 Landscape + + A4 + 0 + 0 + Landscape + 7 + 7 + 7 + 7 + + 75 + 60 + + + + Счет по форме 1С + sale.order + qweb-pdf + l10n_ru_doc.report_order + l10n_ru_doc.report_order + 'Счет - %s ' % (object.name+' '+(object.partner_id.parent_id.name if object.partner_id.parent_id else object.partner_id.name)) + + + report + + + + Счет-фактура + account.move + qweb-pdf + l10n_ru_doc.report_invoice + l10n_ru_doc.report_invoice + 'Счет-фактура - %s ' % (object.name+' '+(object.partner_id.parent_id.name if object.partner_id.parent_id else object.partner_id.name)) + + + report + + + + Товарная накладная (ТОРГ-12) + account.move + qweb-pdf + l10n_ru_doc.report_bill + l10n_ru_doc.report_bill + 'Товарная накладная - %s ' % (object.name+' '+(object.partner_id.parent_id.name if object.partner_id.parent_id else object.partner_id.name)) + + + report + + + + Акт выполненных работ + account.move + qweb-pdf + l10n_ru_doc.report_act + l10n_ru_doc.report_act + 'Акт - %s ' % (object.name+' '+(object.partner_id.parent_id.name if object.partner_id.parent_id else object.partner_id.name)) + + + report + + + + Универсальный передаточный документ(УПД) + account.move + qweb-pdf + l10n_ru_doc.report_upd + l10n_ru_doc.report_upd + 'УПД' + + + report + + + + УПД без печатей + account.move + qweb-pdf + l10n_ru_doc.report_updn + l10n_ru_doc.report_updn + 'УПД без печатей' + + + report + + + diff --git a/l10n_ru_doc/report/report_act.py b/l10n_ru_doc/report/report_act.py new file mode 100644 index 0000000..133336e --- /dev/null +++ b/l10n_ru_doc/report/report_act.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from odoo import models +from odoo.addons.l10n_ru_doc.report_helper import QWebHelper + +class RuActReport(models.AbstractModel): + _name = 'report.l10n_ru_doc.report_act' + + def _get_report_values(self, docids, data=None): + docs = self.env['account.move'].browse(docids) + return { + 'helper': QWebHelper(), + 'doc_ids': docs.ids, + 'doc_model': 'account.move', + 'docs': docs + } diff --git a/l10n_ru_doc/report/report_act.xml b/l10n_ru_doc/report/report_act.xml new file mode 100644 index 0000000..4af4ce3 --- /dev/null +++ b/l10n_ru_doc/report/report_act.xml @@ -0,0 +1,203 @@ + + + + + + diff --git a/l10n_ru_doc/report/report_bill.py b/l10n_ru_doc/report/report_bill.py new file mode 100644 index 0000000..400d3ad --- /dev/null +++ b/l10n_ru_doc/report/report_bill.py @@ -0,0 +1,14 @@ +from odoo import models +from odoo.addons.l10n_ru_doc.report_helper import QWebHelper + +class RuBillReport(models.AbstractModel): + _name = 'report.l10n_ru_doc.report_bill' + + def _get_report_values(self, docids, data=None): + docs = self.env['account.move'].browse(docids) + return { + 'helper': QWebHelper(), + 'doc_ids': docs.ids, + 'doc_model': 'account.move', + 'docs': docs + } diff --git a/l10n_ru_doc/report/report_bill.xml b/l10n_ru_doc/report/report_bill.xml new file mode 100644 index 0000000..c7f5568 --- /dev/null +++ b/l10n_ru_doc/report/report_bill.xml @@ -0,0 +1,758 @@ + + + + + + diff --git a/l10n_ru_doc/report/report_invoice.py b/l10n_ru_doc/report/report_invoice.py new file mode 100644 index 0000000..a71c09a --- /dev/null +++ b/l10n_ru_doc/report/report_invoice.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from odoo import models +from odoo.addons.l10n_ru_doc.report_helper import QWebHelper +class RuInvoiceReport(models.AbstractModel): + _name = 'report.l10n_ru_doc.report_invoice' + def _get_report_values(self, docids, data=None): + docs = self.env['account.move'].browse(docids) + return { + 'helper': QWebHelper(), + 'doc_ids': docs.ids, + 'doc_model': 'account.move', + 'docs': docs + } diff --git a/l10n_ru_doc/report/report_invoice.xml b/l10n_ru_doc/report/report_invoice.xml new file mode 100644 index 0000000..78b8b21 --- /dev/null +++ b/l10n_ru_doc/report/report_invoice.xml @@ -0,0 +1,329 @@ + + + + + + diff --git a/l10n_ru_doc/report/report_order.py b/l10n_ru_doc/report/report_order.py new file mode 100644 index 0000000..eb94aca --- /dev/null +++ b/l10n_ru_doc/report/report_order.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from odoo import models +from odoo.addons.l10n_ru_doc.report_helper import QWebHelper +class RuSaleOrderReport(models.AbstractModel): + _name = 'report.l10n_ru_doc.report_order' + def _get_report_values(self, docids, data=None): + docs = self.env['sale.order'].browse(docids) + return { + 'helper': QWebHelper(), + 'doc_ids': docs.ids, + 'doc_model': 'sale.order', + 'docs': docs + } diff --git a/l10n_ru_doc/report/report_order.xml b/l10n_ru_doc/report/report_order.xml new file mode 100644 index 0000000..f9d2127 --- /dev/null +++ b/l10n_ru_doc/report/report_order.xml @@ -0,0 +1,330 @@ + + + + + + diff --git a/l10n_ru_doc/report/report_upd.py b/l10n_ru_doc/report/report_upd.py new file mode 100644 index 0000000..1537ba8 --- /dev/null +++ b/l10n_ru_doc/report/report_upd.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from odoo import models +from odoo.addons.l10n_ru_doc.report_helper import QWebHelper + +class RuUpdReport(models.AbstractModel): + _name = 'report.l10n_ru_doc.report_upd' + def _get_report_values(self, docids, data=None): + docs = self.env['account.move'].browse(docids) + return { + 'helper': QWebHelper(), + 'doc_ids': docs.ids, + 'doc_model': 'account.move', + 'docs': docs + } +class RuUpdReportn(models.AbstractModel): + _name = 'report.l10n_ru_doc.report_updn' + def _get_report_values(self, docids, data=None): + docs = self.env['account.move'].browse(docids) + return { + 'helper': QWebHelper(), + 'doc_ids': docs.ids, + 'doc_model': 'account.move', + 'docs': docs + } diff --git a/l10n_ru_doc/report/report_upd.xml b/l10n_ru_doc/report/report_upd.xml new file mode 100644 index 0000000..1825c87 --- /dev/null +++ b/l10n_ru_doc/report/report_upd.xml @@ -0,0 +1,1108 @@ + + + + + + diff --git a/l10n_ru_doc/report/report_updn.xml b/l10n_ru_doc/report/report_updn.xml new file mode 100644 index 0000000..b3013c4 --- /dev/null +++ b/l10n_ru_doc/report/report_updn.xml @@ -0,0 +1,1177 @@ + + + + + + diff --git a/l10n_ru_doc/report_helper.py b/l10n_ru_doc/report_helper.py new file mode 100644 index 0000000..4ae4803 --- /dev/null +++ b/l10n_ru_doc/report_helper.py @@ -0,0 +1,112 @@ +from datetime import datetime +import re +from pytils import numeral, dt +from odoo.tools import pycompat + + +class QWebHelper(object): + + def img(self, img, type='png', width=0, height=0) : + if width : + width = "width='%spx'"%(width) + else : + width = " " + if height : + height = "height='%spx'"%(height) + else : + height = " " + toreturn = ""%( + width, + height, + type, + str(pycompat.to_text(img))) + return toreturn + + def numer(self, name): + if name: + numeration = re.findall(r'\d+$', name) + if numeration: + return numeration[0] + return '' + + def ru_date(self, date): + if date and date != 'False': + return dt.ru_strftime('"%d" %B %Y года', date=datetime.strptime(str(date), + "%Y-%m-%d"), inflected=True) + return '' + + def ru_date2(self, date): + if date and date != 'False': + return dt.ru_strftime('%d %B %Y г.', date=datetime.strptime(str(date), + "%Y-%m-%d %H:%M:%S"), inflected=True) + return '' + + def in_words(self, number): + return numeral.in_words(number) + + def rubles(self, sum): + text_rubles = numeral.rubles(int(sum)) + copeck = round((sum - int(sum))*100) + text_copeck = numeral.choose_plural(int(copeck), ("копейка", "копейки", "копеек")) + return ("%s %02d %s")%(text_rubles, copeck, text_copeck) + + def initials(self, fio): + if fio: + return (fio.split()[0]+' '+''.join([fio[0:1]+'.' for fio in fio.split()[1:]])).strip() + return '' + + def address(self, partner): + repr = [] + if partner.zip: + repr.append(partner.zip) + if partner.country_id: + repr.append(partner.country_id.name) + if partner.state_id: + repr.append(partner.state_id.name) + if partner.city: + repr.append(partner.city) + if partner.street: + repr.append(partner.street) + if partner.street2: + repr.append(partner.street2) + return ', '.join(repr) + + def representation(self, partner): + repr = [] + if partner.name: + repr.append(partner.name) + if partner.inn: + repr.append("ИНН " + partner.inn) + if partner.kpp: + repr.append("КПП " + partner.kpp) + repr.append(self.address(partner)) + return ', '.join(repr) + + def full_representation(self, partner): + repr = [self.representation(partner)] + if partner.phone: + repr.append("тел.: " + partner.phone) + elif partner.parent_id.phone: + repr.append("тел.: " + partner.parent_id.phone) + bank = None + if partner.bank_ids: + bank = partner.bank_ids[0] + elif partner.parent_id.bank_ids: + bank = partner.parent_id.bank_ids[0] + if bank and bank.acc_number: + repr.append("р/сч " + bank.acc_number) + if bank and bank.bank_name: + repr.append("в банке " + bank.bank_name) + if bank and bank.banvk_bic: + repr.append("БИК " + bank.bank_bic) + if bank and bank.bank_corr_acc: + repr.append("к/с " + bank.bank_corr_acc) + return ', '.join(repr) + + def representation_small(self, partner): + repr = [] + if partner.name: + repr.append(partner.name) + + repr.append(self.address(partner)) + return ', '.join(repr) diff --git a/l10n_ru_doc/static/description/docs.png b/l10n_ru_doc/static/description/docs.png new file mode 100644 index 0000000..7cc91a8 Binary files /dev/null and b/l10n_ru_doc/static/description/docs.png differ diff --git a/l10n_ru_doc/static/description/icon.png b/l10n_ru_doc/static/description/icon.png new file mode 100644 index 0000000..5b3a94e Binary files /dev/null and b/l10n_ru_doc/static/description/icon.png differ diff --git a/l10n_ru_doc/static/description/index.html b/l10n_ru_doc/static/description/index.html new file mode 100644 index 0000000..d4c9e96 --- /dev/null +++ b/l10n_ru_doc/static/description/index.html @@ -0,0 +1,45 @@ +
+
+

Первичные документы РФ

+
+

+Модуль для печати первичных документов в соответствии с законами РФ. +

+

+Возможности: +

    +
  • Товарная накладная (ТОРГ-12)
  • +
  • Счет на оплату
  • +
  • Счет-фактура
  • +
  • Акт выполненных работ
  • +
  • Вывод подписей и печати
  • +
+

+
+
+
+ +
+
+
+
+ +
+
+

Помощь и поддержка

+
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/l10n_ru_doc/static/description/support.png b/l10n_ru_doc/static/description/support.png new file mode 100644 index 0000000..2805ec7 Binary files /dev/null and b/l10n_ru_doc/static/description/support.png differ diff --git a/l10n_ru_doc/static/description/waybill.png b/l10n_ru_doc/static/description/waybill.png new file mode 100644 index 0000000..8bab618 Binary files /dev/null and b/l10n_ru_doc/static/description/waybill.png differ diff --git a/l10n_ru_doc/static/src/css/l10n_ru_doc.css b/l10n_ru_doc/static/src/css/l10n_ru_doc.css new file mode 100644 index 0000000..d77b69a --- /dev/null +++ b/l10n_ru_doc/static/src/css/l10n_ru_doc.css @@ -0,0 +1,7 @@ +@charset "utf-8"; +.openerp .codup_sign > img { + width: 235px; + height: 65px; + max-width: 235px; + max-height: 65px; +} \ No newline at end of file diff --git a/l10n_ru_doc/views/account_invoice_view.xml b/l10n_ru_doc/views/account_invoice_view.xml new file mode 100644 index 0000000..1d701fa --- /dev/null +++ b/l10n_ru_doc/views/account_invoice_view.xml @@ -0,0 +1,36 @@ + + + + + + account.invoice.ru.form + account.move + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_ru_doc/views/l10n_ru_doc_data.xml b/l10n_ru_doc/views/l10n_ru_doc_data.xml new file mode 100644 index 0000000..169bcd7 --- /dev/null +++ b/l10n_ru_doc/views/l10n_ru_doc_data.xml @@ -0,0 +1,14 @@ + + + + + + + 1 + + + + + + + diff --git a/l10n_ru_doc/views/product.xml b/l10n_ru_doc/views/product.xml new file mode 100644 index 0000000..7fea41d --- /dev/null +++ b/l10n_ru_doc/views/product.xml @@ -0,0 +1,16 @@ + + + + + tnved_product + product.product + + + + + + + + + + diff --git a/l10n_ru_doc/views/res_bank_view.xml b/l10n_ru_doc/views/res_bank_view.xml new file mode 100644 index 0000000..7468aac --- /dev/null +++ b/l10n_ru_doc/views/res_bank_view.xml @@ -0,0 +1,18 @@ + + + + + + res.bank.ru.form + res.bank + form + + + + + + + + + + diff --git a/l10n_ru_doc/views/res_company_view.xml b/l10n_ru_doc/views/res_company_view.xml new file mode 100644 index 0000000..6342ad2 --- /dev/null +++ b/l10n_ru_doc/views/res_company_view.xml @@ -0,0 +1,42 @@ + + + + + + res.companyruform + res.company + + + + + + + + + + ИНН + + + ОГРН + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_ru_doc/views/res_partner_view.xml b/l10n_ru_doc/views/res_partner_view.xml new file mode 100644 index 0000000..016701b --- /dev/null +++ b/l10n_ru_doc/views/res_partner_view.xml @@ -0,0 +1,36 @@ + + + + + + res.partner.ru.form + res.partner + + + + + + + + + + ИНН + + + + + + + + + + + + + + + + + + + diff --git a/l10n_ru_doc/views/res_users_view.xml b/l10n_ru_doc/views/res_users_view.xml new file mode 100644 index 0000000..e7ae0cf --- /dev/null +++ b/l10n_ru_doc/views/res_users_view.xml @@ -0,0 +1,33 @@ + + + + + + res.users.signature.form + res.users + + + + + + + + + + + + res.users.simple.form + res.users + + + + + + + + + + + + + diff --git a/l10n_ru_doc/views/tax.xml b/l10n_ru_doc/views/tax.xml new file mode 100644 index 0000000..3cefac6 --- /dev/null +++ b/l10n_ru_doc/views/tax.xml @@ -0,0 +1,17 @@ + + + + + view_taxiher_form + account.tax + + + + + + + + + + + diff --git a/l10n_ru_doc/views/uom.xml b/l10n_ru_doc/views/uom.xml new file mode 100644 index 0000000..08e8b0c --- /dev/null +++ b/l10n_ru_doc/views/uom.xml @@ -0,0 +1,28 @@ + + + + + view_uomiher_form + uom.uom + + + + + + + + + + view_uomiher_tree + uom.category + + + + + + + + + + + diff --git a/l10n_ru_upd_xml/__init__.py b/l10n_ru_upd_xml/__init__.py new file mode 100644 index 0000000..5a79dbb --- /dev/null +++ b/l10n_ru_upd_xml/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models +from . import reports diff --git a/l10n_ru_upd_xml/__manifest__.py b/l10n_ru_upd_xml/__manifest__.py new file mode 100644 index 0000000..4cb612d --- /dev/null +++ b/l10n_ru_upd_xml/__manifest__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Печать УПД в xml формате", + + 'summary': "Формирует УПД в формате XML, формат 5.01", + + 'description': """ +Формирует УПД в формате XML, формат 5.01 + """, + + 'author': "MKLab", + 'website': "https://inf-centre.ru", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Localization', + 'version': '17.0.1.230710', + + # any module necessary for this one to work correctly + 'depends': ['web','base','account','l10n_ru_doc','contract'], + + # always loaded + 'data': [ + 'views/ir_actions_report_view.xml', + 'views/res_partner_view.xml', + 'views/res_company_view.xml', + 'views/res_users_view.xml', + 'views/views_uom_okei.xml', + 'views/view_move.xml', + 'reports/report.xml', + 'reports/upd_report.xml', + ], + + 'assets': { + 'web.assets_backend': [ + 'upd_xml/static/src/js/report/action_manager_report.js', + ], + }, + # only loaded in demonstration mode + 'demo': [ + 'demo/demo.xml', + ], + + 'external_dependencies': { + 'python': [ + 'lxml' + ] + }, +} + diff --git a/l10n_ru_upd_xml/__pycache__/__init__.cpython-310.pyc b/l10n_ru_upd_xml/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f00f6c6 Binary files /dev/null and b/l10n_ru_upd_xml/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/controllers/__init__.py b/l10n_ru_upd_xml/controllers/__init__.py new file mode 100644 index 0000000..b0f26a9 --- /dev/null +++ b/l10n_ru_upd_xml/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers diff --git a/l10n_ru_upd_xml/controllers/__pycache__/__init__.cpython-310.pyc b/l10n_ru_upd_xml/controllers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d0fb94f Binary files /dev/null and b/l10n_ru_upd_xml/controllers/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/controllers/__pycache__/controllers.cpython-310.pyc b/l10n_ru_upd_xml/controllers/__pycache__/controllers.cpython-310.pyc new file mode 100644 index 0000000..fbaedf3 Binary files /dev/null and b/l10n_ru_upd_xml/controllers/__pycache__/controllers.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/controllers/controllers.py b/l10n_ru_upd_xml/controllers/controllers.py new file mode 100644 index 0000000..537e3a8 --- /dev/null +++ b/l10n_ru_upd_xml/controllers/controllers.py @@ -0,0 +1,91 @@ +# Copyright (C) 2014-2015 Grupo ESOC +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +import json +import logging + +from werkzeug.urls import url_parse + +from odoo.http import content_disposition, request, route, serialize_exception +from odoo.tools import html_escape +from odoo.tools.safe_eval import safe_eval, time + +from odoo.addons.web.controllers import report + +_logger = logging.getLogger(__name__) + + +class ReportController(report.ReportController): + @route() + def report_routes( + self, reportname, docids=None, converter=None, options=None, **kwargs + ): + if converter != "xml": + return super().report_routes( + reportname, + docids=docids, + converter=converter, + options=options, + **kwargs, + ) + if docids: + docids = [int(_id) for _id in docids.split(",")] + data = {**json.loads(options or "{}"), **kwargs} + context = dict(request.env.context) + if "context" in data: + data["context"] = json.loads(data["context"] or "{}") + # Ignore 'lang' here, because the context in data is the one from the + # webclient *but* if the user explicitely wants to change the lang, this + # mechanism overwrites it. + if "lang" in data["context"]: + del data["context"]["lang"] + context.update(data["context"]) + report_Obj = request.env["ir.actions.report"] + xml = report_Obj.with_context(**context)._render_qweb_xml( + reportname, docids, data=data + )[0] + xmlhttpheaders = [("Content-Type", "text/xml"), ("Content-Length", len(xml))] + return request.make_response(xml, headers=xmlhttpheaders) + + @route() + def report_download(self, data, context=None, token=None): + requestcontent = json.loads(data) + url, report_type = requestcontent[0], requestcontent[1] + reportname = "???" + if report_type != "qweb-xml": + return super().report_download(data, context=context, token=token) + try: + reportname = url.split("/report/xml/")[1].split("?")[0] + docids = None + if "/" in reportname: + reportname, docids = reportname.split("/") + report = request.env["ir.actions.report"]._get_report_from_name(reportname) + filename = None + if docids: + response = self.report_routes( + reportname, docids=docids, converter="xml", context=context + ) + ids = [int(x) for x in docids.split(",")] + obj = request.env[report.model].browse(ids) + if report.print_report_name and not len(obj) > 1: + report_name = safe_eval( + report.print_report_name, {"object": obj, "time": time} + ) + filename = f"{report_name}.{report.xml_extension}" + else: + data = url_parse(url).decode_query(cls=dict) + if "context" in data: + context = json.loads(context or "{}") + data_context = json.loads(data.pop("context")) + context = json.dumps({**context, **data_context}) + response = self.report_routes( + reportname, converter="xml", context=context, **data + ) + filename = filename or f"{report.name}.{report.xml_extension}" + response.headers.add("Content-Disposition", content_disposition(filename)) + return response + except Exception as e: + _logger.exception(f"Error while generating report {reportname}") + se = serialize_exception(e) + error = {"code": 200, "message": "Odoo Server Error", "data": se} + return request.make_response(html_escape(json.dumps(error))) \ No newline at end of file diff --git a/l10n_ru_upd_xml/demo/demo.xml b/l10n_ru_upd_xml/demo/demo.xml new file mode 100644 index 0000000..b5678e1 --- /dev/null +++ b/l10n_ru_upd_xml/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/l10n_ru_upd_xml/models/__init__.py b/l10n_ru_upd_xml/models/__init__.py new file mode 100644 index 0000000..6c43466 --- /dev/null +++ b/l10n_ru_upd_xml/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from . import ir_actions_report +from . import res_company +from . import res_partner +from . import res_users +from . import uom_okei +from . import move diff --git a/l10n_ru_upd_xml/models/__pycache__/__init__.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..069b0db Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/__pycache__/ir_actions_report.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/ir_actions_report.cpython-310.pyc new file mode 100644 index 0000000..bd05cb0 Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/ir_actions_report.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/__pycache__/move.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/move.cpython-310.pyc new file mode 100644 index 0000000..485d4bb Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/move.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/__pycache__/res_company.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/res_company.cpython-310.pyc new file mode 100644 index 0000000..384dab7 Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/res_company.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/__pycache__/res_partner.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/res_partner.cpython-310.pyc new file mode 100644 index 0000000..de5e486 Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/res_partner.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/__pycache__/res_users.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/res_users.cpython-310.pyc new file mode 100644 index 0000000..7a5e740 Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/res_users.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/__pycache__/uom_okei.cpython-310.pyc b/l10n_ru_upd_xml/models/__pycache__/uom_okei.cpython-310.pyc new file mode 100644 index 0000000..c2caf92 Binary files /dev/null and b/l10n_ru_upd_xml/models/__pycache__/uom_okei.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/models/ir_actions_report.py b/l10n_ru_upd_xml/models/ir_actions_report.py new file mode 100755 index 0000000..db8e07e --- /dev/null +++ b/l10n_ru_upd_xml/models/ir_actions_report.py @@ -0,0 +1,65 @@ +# Copyright (C) 2014-2015 Grupo ESOC +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +from odoo import fields, models, api + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + report_type = fields.Selection( + selection_add=[("qweb-xml", "XML")], ondelete={"qweb-xml": "set default"} + ) + xsd_schema = fields.Binary( + string="XSD Validation Schema", + attachment=True, + help="File with XSD Schema for checking content of result report. Can be empty " + "if validation is not required.", + ) + xml_encoding = fields.Selection( + selection=[ + ("WINDOWS-1251", "WINDOWS-1251") # will be used as default even if nothing is selected + ], + string="XML Encoding", + help=( + "Encoding for XML reports. If nothing is selected, " + "then UTF-8 will be applied." + ), + ) + xml_declaration = fields.Boolean( + string="XML Declaration", + help=( + """Add `` at the start """ + """of final report file.""" + ), + default=True, + ) + xml_extension = fields.Char( + default="xml", + help="Extension for XML Reports, by default is `xml`", + ) + + @api.model + def _render_qweb_xml(self, report_ref, res_ids, data=None): + """ + Call `generate_report` method of report abstract class + `report.` or of standard class for XML report + rendering - `report.upd_xml.abstract` + + Args: + * docids(list) - IDs of instances for those report will be generated + * data(dict, None) - variables for report rendering + + Returns: + * str - result content of report + * str - type of result content + """ + report = self._get_report(report_ref) + report_model = self.env.get( + f"report.{report.report_name}", self.env["report.upd_xml.abstract"] + ) + return report_model.generate_report( + ir_report=report, # will be used to get settings of report + docids=res_ids, + data=data or {}, + ) diff --git a/l10n_ru_upd_xml/models/move.py b/l10n_ru_upd_xml/models/move.py new file mode 100755 index 0000000..208e7b9 --- /dev/null +++ b/l10n_ru_upd_xml/models/move.py @@ -0,0 +1,209 @@ +from odoo import api, fields, models +import hashlib +from odoo.exceptions import UserError +from odoo.http import request + +class Users(models.Model): + _inherit = 'account.move' + + edi = fields.Char(string='ID EDI', compute='sh1_edi') + kpp = fields.Char(string='КПП', compute='get_kpp') + def get_kpp(self): + for s in self: + pid=self.partner_id.parent_id or self.partner_id + + s.kpp=pid.kpp if (s.partner_id==s.partner_shipping_id or not s.partner_shipping_id) else s.partner_shipping_id.kpp + + @api.depends('name') + def sh1_edi(self): + hash_object = hashlib.sha1((self.name).encode('utf-8')) + pid=self.partner_id.parent_id or self.partner_id + self.edi='ON_NSCHFDOPPR_2BM-'+str(pid.edi)+'_'+str(self.company_id.edi)+'_'+hash_object.hexdigest() + + def print_upd(self): + for s in self: + mes = str(s.check_correct_upd()).strip() + if mes != "": + raise UserError(u"Не удалось сформировать УПД. Выявлены следующие ошибки:\n{}".format(mes)) + else: + # pdf = \ + return self.env.ref('upd_xml.upd_xml_report').sudo().report_action(s.id) # render_qweb_xml(s.id) + # pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf)), ] + # return request.make_response(pdf, headers=pdfhttpheaders) + + def check_correct_upd(self, manually=True): + for s in self: + mes = "" + company = s.company_id + if s.name == '/': + mes += u"Отсутствует наименование документа. Проверидите документ, чтобы назваие сформировалось автоматически.\n" + if not s.only_service and s.get_delivery_doc_name()=='0': + mes += u"Отсутствуют связанные отгрузки.\n" + if not company: + company = self.env.company + if not company: + mes += u"Не указана компания.\n" + else: + if not company.edi: + mes += u"Не указан идентификатор компании для Diadoc.\n" + if not company.name: + mes += u"Не указано наименование компании.\n" + if not company.okpo: + mes += u"Не указано ОКПО компании.\n" + if not company.inn: + mes += u"Не указан ИНН компании.\n" + else: + if len(company.inn) == 12: + if not company.partner_id.last_name_IP: + mes += u"Не указана фамилия ИП для вашей компании.\n" + if not company.partner_id.first_name_IP: + mes += u"Не указано имя ИП для вашей компании.\n" + if not company.partner_id.middle_name_IP: + mes += u"Не указано отчество ИП для вашей компании.\n" + elif len(company.inn) == 10: + if not company.kpp: + mes += u"Не указан КПП компании.\n" + else: + mes += u"Некорректный ИНН компании.\n" + if not company.city: + mes += u"Не указан город компании.\n" + if not company.street: + mes += u"Не указан адрес компании.\n" + if not company.chief_id: + mes += u"Не указан руководитель компании.\n" + else: + if not company.chief_id.function: + mes += u"Не указана должность руководителя компании.\n" + if not company.chief_id.last_name: + mes += u"Не указана фамилия руководителя компании.\n" + if not company.chief_id.first_name: + mes += u"Не указано имя руководителя компании.\n" + if not company.chief_id.second_name: + mes += u"Не указано отчество руководителя компании.\n" + pid = s.partner_id.parent_id + if not pid: + pid = s.partner_id + if not pid: + mes += u"Не указан контрагент.\n" + else: + if not pid.edi: + mes += u"Не указан идентификатор контрагента для Diadoc.\n" + if not pid.name: + mes += u"Не указано наименование контрагента.\n" + if not pid.okpo: + mes += u"Не указано ОКПО контрагента.\n" + if not pid.inn: + mes += u"Не указан ИНН контрагента.\n" + else: + if len(pid.inn) == 12: + if not pid.last_name_IP: + mes += u"Не указана фамилия ИП для контрагента.\n" + if not pid.first_name_IP: + mes += u"Не указано имя ИП для контрагента.\n" + if not pid.middle_name_IP: + mes += u"Не указано отчество ИП для контрагента.\n" + elif len(pid.inn) == 10: + if not pid.kpp: + mes += u"Не указан КПП контрагента.\n" + else: + mes += u"Некорректный ИНН контрагента.\n" + if not pid.city: + mes += u"Не указан город контрагента.\n" + if not pid.street: + mes += u"Не указан адрес контрагента.\n" + if manually: + if not s.edi: + mes += u"Не указан идентификатор документа для Diadoc.\n" + if not s.name: + mes += u"Не указано наименование документа\n" + if not s.invoice_date: + mes += u"Не указана дата документа\n" + if not s.only_service: + gruzootpr = s.gruzootpr + if not gruzootpr: + gruzootpr = pid + if gruzootpr.parent_id: + gruzootpr = gruzootpr.parent_id + if not gruzootpr: + mes += u"Не указан грузоотправитель.\n" + else: + if not gruzootpr.name: + mes += u"Не указано наименование грузоотправителя.\n" + if not gruzootpr.okpo: + mes += u"Не указано ОКПО грузоотправителя.\n" + if not gruzootpr.inn: + mes += u"Не указан ИНН грузоотправителя.\n" + else: + if len(gruzootpr.inn) == 12: + if not gruzootpr.last_name_IP: + mes += u"Не указана фамилия ИП для грузоотправителя.\n" + if not gruzootpr.first_name_IP: + mes += u"Не указано имя ИП для грузоотправителя.\n" + if not gruzootpr.middle_name_IP: + mes += u"Не указано отчество ИП для грузоотправителя.\n" + elif len(gruzootpr.inn) == 10: + if not gruzootpr.kpp: + mes += u"Не указан КПП грузоотправителя.\n" + else: + mes += u"Некорректный ИНН грузоотправителя.\n" + if not gruzootpr.city: + mes += u"Не указан город грузоотправителя.\n" + if not gruzootpr.street: + mes += u"Не указан адрес грузоотправителя.\n" + gruzopol = s.gruzopol + if not gruzopol: + gruzopol = pid + if gruzopol.parent_id: + gruzopol = gruzopol.parent_id + if not gruzopol: + mes += u"Не указан грузополучатель.\n" + else: + if not gruzopol.name: + mes += u"Не указано наименование грузополучателя.\n" + if not gruzopol.okpo: + mes += u"Не указано ОКПО грузополучателя.\n" + if not gruzopol.inn: + mes += u"Не указан ИНН грузополучателя.\n" + else: + if len(gruzopol.inn) == 12: + if not gruzopol.last_name_IP: + mes += u"Не указана фамилия ИП для грузополучателя.\n" + if not gruzopol.first_name_IP: + mes += u"Не указано имя ИП для грузополучателя.\n" + if not gruzopol.middle_name_IP: + mes += u"Не указано отчество ИП для грузополучателя.\n" + elif len(gruzopol.inn) == 10: + if not gruzopol.kpp: + mes += u"Не указан КПП грузополучателя.\n" + else: + mes += u"Некорректный ИНН грузополучателя.\n" + if not gruzopol.city: + mes += u"Не указан город грузополучателя.\n" + if not gruzopol.street: + mes += u"Не указан адрес грузополучателя.\n" + if s.payment_num: + if not s.payment_date: + mes += u"Не указана дата платежки в УПД.\n" + if not s.invoice_line_ids: + mes += u"Отсутствуют строки заказа.\n" + else: + for line in s.invoice_line_ids: + if not line.price_unit: + mes += u"Не указана цена за единицу для товара {}.\n".format(line.name) + if not line.quantity: + mes += u"Не указано количество для товара {}.\n".format(line.name) + if not line.product_uom_id.okei: + mes += u"Не указан код ОКЕИ для единицы измерения {}.\n".format(line.product_uom_id.name) + if not s.mt_contractid: + mes += u"Не указан договор.\n" + else: + if not s.mt_contractid.name: + mes += u"Не указано наименование договора.\n" + if not s.mt_contractid.date_start: + mes += u"Не указана дата договора.\n" + if not s.kladov: + mes += u"Не указано лицо, ответственное за передачу товаров/услуг.\n" + else: + if not s.kladov.partner_id.function: + mes += u"Не указана должность лица, ответственного за передачу товаров/услуг.\n" + return str(mes) \ No newline at end of file diff --git a/l10n_ru_upd_xml/models/res_company.py b/l10n_ru_upd_xml/models/res_company.py new file mode 100755 index 0000000..945d134 --- /dev/null +++ b/l10n_ru_upd_xml/models/res_company.py @@ -0,0 +1,6 @@ +from odoo import api, fields, models + +class Company(models.Model): + _inherit = 'res.company' + + edi = fields.Char(string='ID EDI', readonly=False) \ No newline at end of file diff --git a/l10n_ru_upd_xml/models/res_partner.py b/l10n_ru_upd_xml/models/res_partner.py new file mode 100755 index 0000000..ddf1f85 --- /dev/null +++ b/l10n_ru_upd_xml/models/res_partner.py @@ -0,0 +1,33 @@ +from odoo import api, fields, models + +class ResPartner(models.Model): + _inherit = 'res.partner' + + edi = fields.Char('ID EDI') + house = fields.Char('Дом') + office = fields.Char('Квартира, офис') + fias_id = fields.Char('Код ФИАС') + last_name_IP = fields.Char('Фамилия ИП', compute='get_fio', readonly=False) + first_name_IP = fields.Char('Имя ИП', compute='get_fio',readonly=False) + middle_name_IP = fields.Char('Отчество ИП', compute='get_fio',readonly=False) + + @api.depends('name') + def get_fio(self): + for s in self: + if s.name: + name = s.name + if name.find('ИП ')!=-1: + name = name[name.find(' ')+1:] + s.last_name_IP = name[:name.find(' ')] + name = name[name.find(' ') + 1:] + s.first_name_IP = name[:name.find(' ')] + name = name[name.find(' ') + 1:] + s.middle_name_IP = name + else: + s.last_name_IP = "" + s.first_name_IP = "" + s.middle_name_IP = "" + else: + s.last_name_IP = "" + s.first_name_IP = "" + s.middle_name_IP = "" \ No newline at end of file diff --git a/l10n_ru_upd_xml/models/res_users.py b/l10n_ru_upd_xml/models/res_users.py new file mode 100755 index 0000000..30a844d --- /dev/null +++ b/l10n_ru_upd_xml/models/res_users.py @@ -0,0 +1,25 @@ +from odoo import api, fields, models + +class Users(models.Model): + _inherit = 'res.users' + + last_name = fields.Char(string='Фамилия', compute='update_name') + first_name = fields.Char(string='Имя', compute='update_name') + second_name = fields.Char(string='Отчество', compute='update_name') + + @api.depends('name') + def update_name(self): + for s in self: + s.last_name = '' + s.first_name = '' + s.second_name = '' + if s.name: + s.last_name = s.name + s.first_name = '' + s.second_name = '' + if len(s.name.split(' ')) == 3: + s.last_name, s.first_name, s.second_name = s.name.split(' ') + if len(s.name.split(' ')) == 4: + s.last_name, s.first_name, second_name1, second_name2 = s.name.split(' ') + s.second_name = second_name1 + ' ' + second_name2 + diff --git a/l10n_ru_upd_xml/models/uom_okei.py b/l10n_ru_upd_xml/models/uom_okei.py new file mode 100755 index 0000000..b40925b --- /dev/null +++ b/l10n_ru_upd_xml/models/uom_okei.py @@ -0,0 +1,12 @@ +from odoo import models, fields, api + + +class uom(models.Model): + _inherit="uom.uom" + + okei = fields.Char(string="Код ОКЕИ") + +class uom_move(models.Model): + _inherit="account.move.line" + + uom_okei = fields.Char(string='Код ОКЕИ',related='product_uom_id.okei') diff --git a/l10n_ru_upd_xml/reports/__init__.py b/l10n_ru_upd_xml/reports/__init__.py new file mode 100755 index 0000000..fff0f44 --- /dev/null +++ b/l10n_ru_upd_xml/reports/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +from . import report_report_xml_abstract diff --git a/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-310.pyc b/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..a0e94a4 Binary files /dev/null and b/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-36.pyc b/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-36.pyc new file mode 100755 index 0000000..2055cb9 Binary files /dev/null and b/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-36.pyc differ diff --git a/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-37.pyc b/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-37.pyc new file mode 100755 index 0000000..9a27f91 Binary files /dev/null and b/l10n_ru_upd_xml/reports/__pycache__/__init__.cpython-37.pyc differ diff --git a/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-310.pyc b/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-310.pyc new file mode 100644 index 0000000..baed717 Binary files /dev/null and b/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-310.pyc differ diff --git a/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-36.pyc b/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-36.pyc new file mode 100755 index 0000000..36474ce Binary files /dev/null and b/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-36.pyc differ diff --git a/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-37.pyc b/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-37.pyc new file mode 100755 index 0000000..52ea2da Binary files /dev/null and b/l10n_ru_upd_xml/reports/__pycache__/report_report_xml_abstract.cpython-37.pyc differ diff --git a/l10n_ru_upd_xml/reports/report.xml b/l10n_ru_upd_xml/reports/report.xml new file mode 100755 index 0000000..bed15ef --- /dev/null +++ b/l10n_ru_upd_xml/reports/report.xml @@ -0,0 +1,14 @@ + + + + + УПД xml + account.move + qweb-xml + upd_xml.demo_report_xml_view + upd_xml.demo_report_xml_view + '%s' % (object.edi) + + report + + diff --git a/l10n_ru_upd_xml/reports/report_report_xml_abstract.py b/l10n_ru_upd_xml/reports/report_report_xml_abstract.py new file mode 100755 index 0000000..e4fd6a5 --- /dev/null +++ b/l10n_ru_upd_xml/reports/report_report_xml_abstract.py @@ -0,0 +1,48 @@ +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +from base64 import b64decode +from xml.dom import minidom + +from lxml import etree + +from odoo import api, models,SUPERUSER_ID +from odoo.exceptions import ValidationError + + +class ReportXmlAbstract(models.AbstractModel): + _name = "report.upd_xml.abstract" + _description = "Abstract XML Report" + + @api.model + def generate_report(self, ir_report, docids, data=None): + data = data or {} + data.setdefault("report_type", "text") + data = ir_report._get_rendering_context(ir_report, docids, data) + + result_bin = ir_report._render_template(ir_report.report_name, data) + + parsed_result_bin = minidom.parseString(result_bin) + result = parsed_result_bin.toprettyxml(indent=" ") + + # remove empty lines + utf8 = "UTF-8" + cp1251="WINDOWS-1251" + result = "\n".join( + line for line in result.splitlines() if line and not line.isspace() + ).encode('utf8') + + content = etree.tostring( + etree.fromstring(result), + encoding=ir_report.xml_encoding or cp1251, + xml_declaration=True, + pretty_print=True, + ) + return content, "xml" + + @api.model + def _get_report_values(self, docids, data=None): + return data or {} + + # if not data: + # data = {} + # return data.decode('cp1251') diff --git a/l10n_ru_upd_xml/reports/upd_report.xml b/l10n_ru_upd_xml/reports/upd_report.xml new file mode 100755 index 0000000..50d47b5 --- /dev/null +++ b/l10n_ru_upd_xml/reports/upd_report.xml @@ -0,0 +1,186 @@ + + + + diff --git a/l10n_ru_upd_xml/security/ir.model.access.csv b/l10n_ru_upd_xml/security/ir.model.access.csv new file mode 100644 index 0000000..7a89480 --- /dev/null +++ b/l10n_ru_upd_xml/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_upd_xml_upd_xml,upd_xml.upd_xml,model_upd_xml_upd_xml,base.group_user,1,1,1,1 diff --git a/l10n_ru_upd_xml/static/src/js/report/action_manager_report.js b/l10n_ru_upd_xml/static/src/js/report/action_manager_report.js new file mode 100755 index 0000000..e83200e --- /dev/null +++ b/l10n_ru_upd_xml/static/src/js/report/action_manager_report.js @@ -0,0 +1,49 @@ +/** @odoo-module **/ + +import {download} from "@web/core/network/download"; +import {registry} from "@web/core/registry"; + +function getReportUrl({report_name, context, data}, env) { + // Rough copy of action_service.js _getReportUrl method. + let url = `/report/xml/${report_name}`; + const actionContext = context || {}; + if (data && JSON.stringify(data) !== "{}") { + const encodedOptions = encodeURIComponent(JSON.stringify(data)); + const encodedContext = encodeURIComponent(JSON.stringify(actionContext)); + return `${url}?options=${encodedOptions}&context=${encodedContext}`; + } + if (actionContext.active_ids) { + url += `/${actionContext.active_ids.join(",")}`; + } + const userContext = encodeURIComponent(JSON.stringify(env.services.user.context)); + return `${url}?context=${userContext}`; +} +async function triggerDownload(action, {onClose}, env) { + // Rough copy of action_service.js _triggerDownload method. + env.services.ui.block(); + const data = JSON.stringify([getReportUrl(action, env), action.report_type]); + const context = JSON.stringify(env.services.user.context); + try { + await download({url: "/report/download", data: {data, context}}); + } finally { + env.services.ui.unblock(); + } + if (action.close_on_report_download) { + return env.services.action.doAction( + {type: "ir.actions.act_window_close"}, + {onClose} + ); + } + if (onClose) { + onClose(); + } +} +registry + .category("ir.actions.report handlers") + .add("xml_handler", async function (action, options, env) { + if (action.report_type === "qweb-xml") { + await triggerDownload(action, options, env); + return true; + } + return false; + }); diff --git a/l10n_ru_upd_xml/views/ir_actions_report_view.xml b/l10n_ru_upd_xml/views/ir_actions_report_view.xml new file mode 100755 index 0000000..d5cd94f --- /dev/null +++ b/l10n_ru_upd_xml/views/ir_actions_report_view.xml @@ -0,0 +1,24 @@ + + + + ir.actions.report.view.form.report.xml + ir.actions.report + + + + + + + + + + + + + diff --git a/l10n_ru_upd_xml/views/res_company_view.xml b/l10n_ru_upd_xml/views/res_company_view.xml new file mode 100755 index 0000000..2d43a2c --- /dev/null +++ b/l10n_ru_upd_xml/views/res_company_view.xml @@ -0,0 +1,19 @@ + + + + + + res.company.ru.form + res.company + + + + + + + + + + + + diff --git a/l10n_ru_upd_xml/views/res_partner_view.xml b/l10n_ru_upd_xml/views/res_partner_view.xml new file mode 100755 index 0000000..c716587 --- /dev/null +++ b/l10n_ru_upd_xml/views/res_partner_view.xml @@ -0,0 +1,26 @@ + + + + + + res.partner.ru.form + res.partner + + + + + + + + + + + + + + + + + + + diff --git a/l10n_ru_upd_xml/views/res_users_view.xml b/l10n_ru_upd_xml/views/res_users_view.xml new file mode 100755 index 0000000..8dc9487 --- /dev/null +++ b/l10n_ru_upd_xml/views/res_users_view.xml @@ -0,0 +1,21 @@ + + + + + + res.users.signature.form + res.users + + + + + + + + + + + + + + diff --git a/l10n_ru_upd_xml/views/view_move.xml b/l10n_ru_upd_xml/views/view_move.xml new file mode 100755 index 0000000..21a699b --- /dev/null +++ b/l10n_ru_upd_xml/views/view_move.xml @@ -0,0 +1,17 @@ + + + + edi account move + account.move + + + + +