From cde14eb640cca8676b83da17dc3c3b3eb1d94527 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 20 Sep 2021 15:50:12 +0530 Subject: [PATCH] feat: Sales Commissoion Payout --- .../doctype/payment_entry/payment_entry.py | 3863 +++++++++-------- .../payroll/doctype/contributions/__init__.py | 0 .../doctype/contributions/contributions.json | 101 + .../doctype/contributions/contributions.py | 9 + .../payroll_settings/payroll_settings.json | 9 +- .../process_sales_commission/__init__.py | 0 .../process_sales_commission.js | 17 + .../process_sales_commission.json | 145 + .../process_sales_commission.py | 98 + .../test_process_sales_commission.py | 9 + .../doctype/sales_commission/__init__.py | 0 .../sales_commission/sales_commission.js | 118 + .../sales_commission/sales_commission.json | 263 ++ .../sales_commission/sales_commission.py | 103 + .../sales_commission/test_sales_commission.py | 9 + .../selling_settings/selling_settings.json | 11 +- 16 files changed, 2819 insertions(+), 1936 deletions(-) create mode 100644 erpnext/payroll/doctype/contributions/__init__.py create mode 100644 erpnext/payroll/doctype/contributions/contributions.json create mode 100644 erpnext/payroll/doctype/contributions/contributions.py create mode 100644 erpnext/payroll/doctype/process_sales_commission/__init__.py create mode 100644 erpnext/payroll/doctype/process_sales_commission/process_sales_commission.js create mode 100644 erpnext/payroll/doctype/process_sales_commission/process_sales_commission.json create mode 100644 erpnext/payroll/doctype/process_sales_commission/process_sales_commission.py create mode 100644 erpnext/payroll/doctype/process_sales_commission/test_process_sales_commission.py create mode 100644 erpnext/payroll/doctype/sales_commission/__init__.py create mode 100644 erpnext/payroll/doctype/sales_commission/sales_commission.js create mode 100644 erpnext/payroll/doctype/sales_commission/sales_commission.json create mode 100644 erpnext/payroll/doctype/sales_commission/sales_commission.py create mode 100644 erpnext/payroll/doctype/sales_commission/test_sales_commission.py diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7257e6d75fb..5b07d1febc2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -11,2133 +11,2138 @@ from frappe.utils import cint, comma_or, flt, getdate, nowdate import erpnext from erpnext.accounts.doctype.bank_account.bank_account import ( - get_bank_account_details, - get_party_bank_account, + get_bank_account_details, + get_party_bank_account, ) from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( - get_party_account_based_on_invoice_discounting, + get_party_account_based_on_invoice_discounting, ) from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( - get_party_tax_withholding_details, + get_party_tax_withholding_details, ) from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices from erpnext.controllers.accounts_controller import ( - AccountsController, - get_supplier_block_status, - validate_taxes_and_charges, + AccountsController, + get_supplier_block_status, + validate_taxes_and_charges, ) from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount from erpnext.setup.utils import get_exchange_rate class InvalidPaymentEntry(ValidationError): - pass + pass class PaymentEntry(AccountsController): - def __init__(self, *args, **kwargs): - super(PaymentEntry, self).__init__(*args, **kwargs) - if not self.is_new(): - self.setup_party_account_field() - - def setup_party_account_field(self): - self.party_account_field = None - self.party_account = None - self.party_account_currency = None - - if self.payment_type == "Receive": - self.party_account_field = "paid_from" - self.party_account = self.paid_from - self.party_account_currency = self.paid_from_account_currency - - elif self.payment_type == "Pay": - self.party_account_field = "paid_to" - self.party_account = self.paid_to - self.party_account_currency = self.paid_to_account_currency - - def validate(self): - self.setup_party_account_field() - self.set_missing_values() - self.validate_payment_type() - self.validate_party_details() - self.validate_bank_accounts() - self.set_exchange_rate() - self.validate_mandatory() - self.validate_reference_documents() - self.set_tax_withholding() - self.set_amounts() - self.validate_amounts() - self.apply_taxes() - self.set_amounts_after_tax() - self.clear_unallocated_reference_document_rows() - self.validate_payment_against_negative_invoice() - self.validate_transaction_reference() - self.set_title() - self.set_remarks() - self.validate_duplicate_entry() - self.validate_payment_type_with_outstanding() - self.validate_allocated_amount() - self.validate_paid_invoices() - self.ensure_supplier_is_not_blocked() - self.set_status() - - def on_submit(self): - if self.difference_amount: - frappe.throw(_("Difference Amount must be zero")) - self.make_gl_entries() - self.update_expense_claim() - self.update_outstanding_amounts() - self.update_advance_paid() - self.update_payment_schedule() - self.set_status() - - def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") - self.make_gl_entries(cancel=1) - self.update_expense_claim() - self.update_outstanding_amounts() - self.update_advance_paid() - self.delink_advance_entry_references() - self.update_payment_schedule(cancel=1) - self.set_payment_req_status() - self.set_status() - - def set_payment_req_status(self): - from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status - - update_payment_req_status(self, None) - - def update_outstanding_amounts(self): - self.set_missing_ref_details(force=True) - - def validate_duplicate_entry(self): - reference_names = [] - for d in self.get("references"): - if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: - frappe.throw( - _("Row #{0}: Duplicate entry in References {1} {2}").format( - d.idx, d.reference_doctype, d.reference_name - ) - ) - reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) - - def set_bank_account_data(self): - if self.bank_account: - bank_data = get_bank_account_details(self.bank_account) - - field = "paid_from" if self.payment_type == "Pay" else "paid_to" - - self.bank = bank_data.bank - self.bank_account_no = bank_data.bank_account_no - - if not self.get(field): - self.set(field, bank_data.account) - - def validate_payment_type_with_outstanding(self): - total_outstanding = sum(d.allocated_amount for d in self.get("references")) - if total_outstanding < 0 and self.party_type == "Customer" and self.payment_type == "Receive": - frappe.throw( - _("Cannot receive from customer against negative outstanding"), - title=_("Incorrect Payment Type"), - ) - - def validate_allocated_amount(self): - for d in self.get("references"): - if (flt(d.allocated_amount)) > 0: - if flt(d.allocated_amount) > flt(d.outstanding_amount): - frappe.throw( - _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx) - ) - - # Check for negative outstanding invoices as well - if flt(d.allocated_amount) < 0: - if flt(d.allocated_amount) < flt(d.outstanding_amount): - frappe.throw( - _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx) - ) - - def delink_advance_entry_references(self): - for reference in self.references: - if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"): - doc = frappe.get_doc(reference.reference_doctype, reference.reference_name) - doc.delink_advance_entries(self.name) - - def set_missing_values(self): - if self.payment_type == "Internal Transfer": - for field in ( - "party", - "party_balance", - "total_allocated_amount", - "base_total_allocated_amount", - "unallocated_amount", - ): - self.set(field, None) - self.references = [] - else: - if not self.party_type: - frappe.throw(_("Party Type is mandatory")) - - if not self.party: - frappe.throw(_("Party is mandatory")) - - _party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name" - self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) - - if self.party: - if not self.party_balance: - self.party_balance = get_balance_on( - party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company - ) - - if not self.party_account: - party_account = get_party_account(self.party_type, self.party, self.company) - self.set(self.party_account_field, party_account) - self.party_account = party_account - - if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance): - acc = get_account_details(self.paid_from, self.posting_date, self.cost_center) - self.paid_from_account_currency = acc.account_currency - self.paid_from_account_balance = acc.account_balance - - if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance): - acc = get_account_details(self.paid_to, self.posting_date, self.cost_center) - self.paid_to_account_currency = acc.account_currency - self.paid_to_account_balance = acc.account_balance - - self.party_account_currency = ( - self.paid_from_account_currency - if self.payment_type == "Receive" - else self.paid_to_account_currency - ) - - self.set_missing_ref_details() - - def set_missing_ref_details(self, force=False): - for d in self.get("references"): - if d.allocated_amount: - ref_details = get_reference_details( - d.reference_doctype, d.reference_name, self.party_account_currency - ) - - for field, value in ref_details.items(): - if d.exchange_gain_loss: - # for cases where gain/loss is booked into invoice - # exchange_gain_loss is calculated from invoice & populated - # and row.exchange_rate is already set to payment entry's exchange rate - # refer -> `update_reference_in_payment_entry()` in utils.py - continue - - if field == "exchange_rate" or not d.get(field) or force: - d.db_set(field, value) - - def validate_payment_type(self): - if self.payment_type not in ("Receive", "Pay", "Internal Transfer"): - frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer")) - - def validate_party_details(self): - if self.party: - if not frappe.db.exists(self.party_type, self.party): - frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party)) - - if self.party_account and self.party_type in ("Customer", "Supplier"): - self.validate_account_type( - self.party_account, [erpnext.get_party_account_type(self.party_type)] - ) - - def validate_bank_accounts(self): - if self.payment_type in ("Pay", "Internal Transfer"): - self.validate_account_type(self.paid_from, ["Bank", "Cash"]) - - if self.payment_type in ("Receive", "Internal Transfer"): - self.validate_account_type(self.paid_to, ["Bank", "Cash"]) - - def validate_account_type(self, account, account_types): - account_type = frappe.db.get_value("Account", account, "account_type") - # if account_type not in account_types: - # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) - - def set_exchange_rate(self, ref_doc=None): - self.set_source_exchange_rate(ref_doc) - self.set_target_exchange_rate(ref_doc) - - def set_source_exchange_rate(self, ref_doc=None): - if self.paid_from and not self.source_exchange_rate: - if self.paid_from_account_currency == self.company_currency: - self.source_exchange_rate = 1 - else: - if ref_doc: - if self.paid_from_account_currency == ref_doc.currency: - self.source_exchange_rate = ref_doc.get("exchange_rate") - - if not self.source_exchange_rate: - self.source_exchange_rate = get_exchange_rate( - self.paid_from_account_currency, self.company_currency, self.posting_date - ) - - def set_target_exchange_rate(self, ref_doc=None): - if self.paid_from_account_currency == self.paid_to_account_currency: - self.target_exchange_rate = self.source_exchange_rate - elif self.paid_to and not self.target_exchange_rate: - if ref_doc: - if self.paid_to_account_currency == ref_doc.currency: - self.target_exchange_rate = ref_doc.get("exchange_rate") - - if not self.target_exchange_rate: - self.target_exchange_rate = get_exchange_rate( - self.paid_to_account_currency, self.company_currency, self.posting_date - ) - - def validate_mandatory(self): - for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): - if not self.get(field): - frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field))) - - def validate_reference_documents(self): - if self.party_type == "Customer": - valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") - elif self.party_type == "Supplier": - valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") - elif self.party_type == "Employee": - valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity") - elif self.party_type == "Shareholder": - valid_reference_doctypes = "Journal Entry" - - for d in self.get("references"): - if not d.allocated_amount: - continue - if d.reference_doctype not in valid_reference_doctypes: - frappe.throw( - _("Reference Doctype must be one of {0}").format(comma_or(valid_reference_doctypes)) - ) - - elif d.reference_name: - if not frappe.db.exists(d.reference_doctype, d.reference_name): - frappe.throw(_("{0} {1} does not exist").format(d.reference_doctype, d.reference_name)) - else: - ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name) - - if d.reference_doctype != "Journal Entry": - if self.party != ref_doc.get(scrub(self.party_type)): - frappe.throw( - _("{0} {1} is not associated with {2} {3}").format( - d.reference_doctype, d.reference_name, self.party_type, self.party - ) - ) - else: - self.validate_journal_entry() - - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Expense Claim", "Fees"): - if self.party_type == "Customer": - ref_party_account = ( - get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to - ) - elif self.party_type == "Supplier": - ref_party_account = ref_doc.credit_to - elif self.party_type == "Employee": - ref_party_account = ref_doc.payable_account - - if ref_party_account != self.party_account: - frappe.throw( - _("{0} {1} is associated with {2}, but Party Account is {3}").format( - d.reference_doctype, d.reference_name, ref_party_account, self.party_account - ) - ) - - if ref_doc.docstatus != 1: - frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name)) - - def validate_paid_invoices(self): - no_oustanding_refs = {} - - for d in self.get("references"): - if not d.allocated_amount: - continue - - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): - outstanding_amount, is_return = frappe.get_cached_value( - d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"] - ) - if outstanding_amount <= 0 and not is_return: - no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) - - for k, v in no_oustanding_refs.items(): - frappe.msgprint( - _( - "{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry." - ).format( - _(k), - frappe.bold(", ".join(d.reference_name for d in v)), - frappe.bold(_("negative outstanding amount")), - ) - + "

" - + _("If this is undesirable please cancel the corresponding Payment Entry."), - title=_("Warning"), - indicator="orange", - ) - - def validate_journal_entry(self): - for d in self.get("references"): - if d.allocated_amount and d.reference_doctype == "Journal Entry": - je_accounts = frappe.db.sql( - """select debit, credit from `tabJournal Entry Account` - where account = %s and party=%s and docstatus = 1 and parent = %s - and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order")) - """, - (self.party_account, self.party, d.reference_name), - as_dict=True, - ) - - if not je_accounts: - frappe.throw( - _( - "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher" - ).format(d.idx, d.reference_name, self.party_account) - ) - else: - dr_or_cr = "debit" if self.payment_type == "Receive" else "credit" - valid = False - for jvd in je_accounts: - if flt(jvd[dr_or_cr]) > 0: - valid = True - if not valid: - frappe.throw( - _("Against Journal Entry {0} does not have any unmatched {1} entry").format( - d.reference_name, dr_or_cr - ) - ) - - def update_payment_schedule(self, cancel=0): - invoice_payment_amount_map = {} - invoice_paid_amount_map = {} - - for ref in self.get("references"): - if ref.payment_term and ref.reference_name: - key = (ref.payment_term, ref.reference_name) - invoice_payment_amount_map.setdefault(key, 0.0) - invoice_payment_amount_map[key] += ref.allocated_amount - - if not invoice_paid_amount_map.get(key): - payment_schedule = frappe.get_all( - "Payment Schedule", - filters={"parent": ref.reference_name}, - fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"], - ) - for term in payment_schedule: - invoice_key = (term.payment_term, ref.reference_name) - invoice_paid_amount_map.setdefault(invoice_key, {}) - invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding - invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( - term.discount / 100 - ) - - for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1): - if not invoice_paid_amount_map.get(key): - frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1])) - - outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) - discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) - - if cancel: - frappe.db.sql( - """ - UPDATE `tabPayment Schedule` - SET - paid_amount = `paid_amount` - %s, - discounted_amount = `discounted_amount` - %s, - outstanding = `outstanding` + %s - WHERE parent = %s and payment_term = %s""", - (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), - ) - else: - if allocated_amount > outstanding: - frappe.throw( - _("Row #{0}: Cannot allocate more than {1} against payment term {2}").format( - idx, outstanding, key[0] - ) - ) - - if allocated_amount and outstanding: - frappe.db.sql( - """ - UPDATE `tabPayment Schedule` - SET - paid_amount = `paid_amount` + %s, - discounted_amount = `discounted_amount` + %s, - outstanding = `outstanding` - %s - WHERE parent = %s and payment_term = %s""", - (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), - ) - - def set_status(self): - if self.docstatus == 2: - self.status = "Cancelled" - elif self.docstatus == 1: - self.status = "Submitted" - else: - self.status = "Draft" - - self.db_set("status", self.status, update_modified=True) - - def set_tax_withholding(self): - if not self.party_type == "Supplier": - return - - if not self.apply_tax_withholding_amount: - return - - net_total = self.paid_amount - - # Adding args as purchase invoice to get TDS amount - args = frappe._dict( - { - "company": self.company, - "doctype": "Payment Entry", - "supplier": self.party, - "posting_date": self.posting_date, - "net_total": net_total, - } - ) - - tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) - - if not tax_withholding_details: - return - - tax_withholding_details.update( - {"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)} - ) - - accounts = [] - for d in self.taxes: - if d.account_head == tax_withholding_details.get("account_head"): - - # Preserve user updated included in paid amount - if d.included_in_paid_amount: - tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount}) - - d.update(tax_withholding_details) - accounts.append(d.account_head) - - if not accounts or tax_withholding_details.get("account_head") not in accounts: - self.append("taxes", tax_withholding_details) - - to_remove = [ - d - for d in self.taxes - if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head") - ] - - for d in to_remove: - self.remove(d) - - def apply_taxes(self): - self.initialize_taxes() - self.determine_exclusive_rate() - self.calculate_taxes() - - def set_amounts(self): - self.set_received_amount() - self.set_amounts_in_company_currency() - self.set_total_allocated_amount() - self.set_unallocated_amount() - self.set_difference_amount() - - def validate_amounts(self): - self.validate_received_amount() - - def validate_received_amount(self): - if self.paid_from_account_currency == self.paid_to_account_currency: - if self.paid_amount < self.received_amount: - frappe.throw(_("Received Amount cannot be greater than Paid Amount")) - - def set_received_amount(self): - self.base_received_amount = self.base_paid_amount - if ( - self.paid_from_account_currency == self.paid_to_account_currency - and not self.payment_type == "Internal Transfer" - ): - self.received_amount = self.paid_amount - - def set_amounts_after_tax(self): - applicable_tax = 0 - base_applicable_tax = 0 - for tax in self.get("taxes"): - if not tax.included_in_paid_amount: - amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount - base_amount = ( - -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount - ) - - applicable_tax += amount - base_applicable_tax += base_amount - - self.paid_amount_after_tax = flt( - flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") - ) - self.base_paid_amount_after_tax = flt( - flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate), - self.precision("base_paid_amount_after_tax"), - ) - - self.received_amount_after_tax = flt( - flt(self.received_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") - ) - self.base_received_amount_after_tax = flt( - flt(self.received_amount_after_tax) * flt(self.target_exchange_rate), - self.precision("base_paid_amount_after_tax"), - ) - - def set_amounts_in_company_currency(self): - self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0 - if self.paid_amount: - self.base_paid_amount = flt( - flt(self.paid_amount) * flt(self.source_exchange_rate), self.precision("base_paid_amount") - ) - - if self.received_amount: - self.base_received_amount = flt( - flt(self.received_amount) * flt(self.target_exchange_rate), - self.precision("base_received_amount"), - ) - - def set_total_allocated_amount(self): - if self.payment_type == "Internal Transfer": - return - - total_allocated_amount, base_total_allocated_amount = 0, 0 - for d in self.get("references"): - if d.allocated_amount: - total_allocated_amount += flt(d.allocated_amount) - base_total_allocated_amount += flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") - ) - - self.total_allocated_amount = abs(total_allocated_amount) - self.base_total_allocated_amount = abs(base_total_allocated_amount) - - def set_unallocated_amount(self): - self.unallocated_amount = 0 - if self.party: - total_deductions = sum(flt(d.amount) for d in self.get("deductions")) - included_taxes = self.get_included_taxes() - if ( - self.payment_type == "Receive" - and self.base_total_allocated_amount < self.base_received_amount + total_deductions - and self.total_allocated_amount - < self.paid_amount + (total_deductions / self.source_exchange_rate) - ): - self.unallocated_amount = ( - self.base_received_amount + total_deductions - self.base_total_allocated_amount - ) / self.source_exchange_rate - self.unallocated_amount -= included_taxes - elif ( - self.payment_type == "Pay" - and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) - and self.total_allocated_amount - < self.received_amount + (total_deductions / self.target_exchange_rate) - ): - self.unallocated_amount = ( - self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) - ) / self.target_exchange_rate - self.unallocated_amount -= included_taxes - - def set_difference_amount(self): - base_unallocated_amount = flt(self.unallocated_amount) * ( - flt(self.source_exchange_rate) - if self.payment_type == "Receive" - else flt(self.target_exchange_rate) - ) - - base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) - - if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount - elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount - base_party_amount - else: - self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - - total_deductions = sum(flt(d.amount) for d in self.get("deductions")) - included_taxes = self.get_included_taxes() - - self.difference_amount = flt( - self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") - ) - - def get_included_taxes(self): - included_taxes = 0 - for tax in self.get("taxes"): - if tax.included_in_paid_amount: - if tax.add_deduct_tax == "Add": - included_taxes += tax.base_tax_amount - else: - included_taxes -= tax.base_tax_amount - - return included_taxes - - # Paid amount is auto allocated in the reference document by default. - # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast - def clear_unallocated_reference_document_rows(self): - self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]})) - frappe.db.sql( - """delete from `tabPayment Entry Reference` - where parent = %s and allocated_amount = 0""", - self.name, - ) - - def validate_payment_against_negative_invoice(self): - if (self.payment_type == "Pay" and self.party_type == "Customer") or ( - self.payment_type == "Receive" and self.party_type == "Supplier" - ): - - total_negative_outstanding = sum( - abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 - ) - - paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount - additional_charges = sum([flt(d.amount) for d in self.deductions]) - - if not total_negative_outstanding: - frappe.throw( - _("Cannot {0} {1} {2} without any negative outstanding invoice").format( - _(self.payment_type), - (_("to") if self.party_type == "Customer" else _("from")), - self.party_type, - ), - InvalidPaymentEntry, - ) - - elif paid_amount - additional_charges > total_negative_outstanding: - frappe.throw( - _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( - total_negative_outstanding - ), - InvalidPaymentEntry, - ) - - def set_title(self): - if frappe.flags.in_import and self.title: - # do not set title dynamically if title exists during data import. - return - - if self.payment_type in ("Receive", "Pay"): - self.title = self.party - else: - self.title = self.paid_from + " - " + self.paid_to - - def validate_transaction_reference(self): - bank_account = self.paid_to if self.payment_type == "Receive" else self.paid_from - bank_account_type = frappe.db.get_value("Account", bank_account, "account_type") - - if bank_account_type == "Bank": - if not self.reference_no or not self.reference_date: - frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) - - def set_remarks(self): - if self.custom_remarks: - return - - if self.payment_type == "Internal Transfer": - remarks = [ - _("Amount {0} {1} transferred from {2} to {3}").format( - self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to - ) - ] - else: - - remarks = [ - _("Amount {0} {1} {2} {3}").format( - self.party_account_currency, - self.paid_amount if self.payment_type == "Receive" else self.received_amount, - _("received from") if self.payment_type == "Receive" else _("to"), - self.party, - ) - ] - - if self.reference_no: - remarks.append( - _("Transaction reference no {0} dated {1}").format(self.reference_no, self.reference_date) - ) - - if self.payment_type in ["Receive", "Pay"]: - for d in self.get("references"): - if d.allocated_amount: - remarks.append( - _("Amount {0} {1} against {2} {3}").format( - self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name - ) - ) - - for d in self.get("deductions"): - if d.amount: - remarks.append( - _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account) - ) - - self.set("remarks", "\n".join(remarks)) - - def make_gl_entries(self, cancel=0, adv_adj=0): - if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"): - self.setup_party_account_field() - - gl_entries = [] - self.add_party_gl_entries(gl_entries) - self.add_bank_gl_entries(gl_entries) - self.add_deductions_gl_entries(gl_entries) - self.add_tax_gl_entries(gl_entries) - - gl_entries = process_gl_map(gl_entries) - make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) - - def add_party_gl_entries(self, gl_entries): - if self.party_account: - if self.payment_type == "Receive": - against_account = self.paid_to - else: - against_account = self.paid_from - - party_gl_dict = self.get_gl_dict( - { - "account": self.party_account, - "party_type": self.party_type, - "party": self.party, - "against": against_account, - "account_currency": self.party_account_currency, - "cost_center": self.cost_center, - }, - item=self, - ) - - dr_or_cr = ( - "credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit" - ) - - for d in self.get("references"): - cost_center = self.cost_center - if d.reference_doctype == "Sales Invoice" and not cost_center: - cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") - gle = party_gl_dict.copy() - gle.update( - { - "against_voucher_type": d.reference_doctype, - "against_voucher": d.reference_name, - "cost_center": cost_center, - } - ) - - allocated_amount_in_company_currency = flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount") - ) - - gle.update( - { - dr_or_cr + "_in_account_currency": d.allocated_amount, - dr_or_cr: allocated_amount_in_company_currency, - } - ) - - gl_entries.append(gle) - - if self.unallocated_amount: - exchange_rate = self.get_exchange_rate() - base_unallocated_amount = self.unallocated_amount * exchange_rate - - gle = party_gl_dict.copy() - - gle.update( - { - dr_or_cr + "_in_account_currency": self.unallocated_amount, - dr_or_cr: base_unallocated_amount, - } - ) - - gl_entries.append(gle) - - def add_bank_gl_entries(self, gl_entries): - if self.payment_type in ("Pay", "Internal Transfer"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.paid_from, - "account_currency": self.paid_from_account_currency, - "against": self.party if self.payment_type == "Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount, - "credit": self.base_paid_amount, - "cost_center": self.cost_center, - "post_net_value": True, - }, - item=self, - ) - ) - if self.payment_type in ("Receive", "Internal Transfer"): - gl_entries.append( - self.get_gl_dict( - { - "account": self.paid_to, - "account_currency": self.paid_to_account_currency, - "against": self.party if self.payment_type == "Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount, - "debit": self.base_received_amount, - "cost_center": self.cost_center, - }, - item=self, - ) - ) - - def add_tax_gl_entries(self, gl_entries): - for d in self.get("taxes"): - account_currency = get_account_currency(d.account_head) - if account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - - if self.payment_type in ("Pay", "Internal Transfer"): - dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" - rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" - against = self.party or self.paid_from - elif self.payment_type == "Receive": - dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" - rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" - against = self.party or self.paid_to - - payment_account = self.get_party_account_for_taxes() - tax_amount = d.tax_amount - base_tax_amount = d.base_tax_amount - - gl_entries.append( - self.get_gl_dict( - { - "account": d.account_head, - "against": against, - dr_or_cr: tax_amount, - dr_or_cr + "_in_account_currency": base_tax_amount - if account_currency == self.company_currency - else d.tax_amount, - "cost_center": d.cost_center, - "post_net_value": True, - }, - account_currency, - item=d, - ) - ) - - if not d.included_in_paid_amount: - gl_entries.append( - self.get_gl_dict( - { - "account": payment_account, - "against": against, - rev_dr_or_cr: tax_amount, - rev_dr_or_cr + "_in_account_currency": base_tax_amount - if account_currency == self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - "post_net_value": True, - }, - account_currency, - item=d, - ) - ) - - def add_deductions_gl_entries(self, gl_entries): - for d in self.get("deductions"): - if d.amount: - account_currency = get_account_currency(d.account) - if account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) - - gl_entries.append( - self.get_gl_dict( - { - "account": d.account, - "account_currency": account_currency, - "against": self.party or self.paid_from, - "debit_in_account_currency": d.amount, - "debit": d.amount, - "cost_center": d.cost_center, - }, - item=d, - ) - ) - - def get_party_account_for_taxes(self): - if self.payment_type == "Receive": - return self.paid_to - elif self.payment_type in ("Pay", "Internal Transfer"): - return self.paid_from - - def update_advance_paid(self): - if self.payment_type in ("Receive", "Pay") and self.party: - for d in self.get("references"): - if d.allocated_amount and d.reference_doctype in ( - "Sales Order", - "Purchase Order", - "Employee Advance", - "Gratuity", - ): - frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() - - def update_expense_claim(self): - if self.payment_type in ("Pay") and self.party: - for d in self.get("references"): - if d.reference_doctype == "Expense Claim" and d.reference_name: - doc = frappe.get_doc("Expense Claim", d.reference_name) - if self.docstatus == 2: - update_reimbursed_amount(doc, -1 * d.allocated_amount) - else: - update_reimbursed_amount(doc, d.allocated_amount) - - def on_recurring(self, reference_doc, auto_repeat_doc): - self.reference_no = reference_doc.name - self.reference_date = nowdate() - - def calculate_deductions(self, tax_details): - return { - "account": tax_details["tax"]["account_head"], - "cost_center": frappe.get_cached_value("Company", self.company, "cost_center"), - "amount": self.total_allocated_amount * (tax_details["tax"]["rate"] / 100), - } - - def set_gain_or_loss(self, account_details=None): - if not self.difference_amount: - self.set_difference_amount() - - row = {"amount": self.difference_amount} - - if account_details: - row.update(account_details) - - if not row.get("amount"): - # if no difference amount - return - - self.append("deductions", row) - self.set_unallocated_amount() - - def get_exchange_rate(self): - return self.source_exchange_rate if self.payment_type == "Receive" else self.target_exchange_rate - - def initialize_taxes(self): - for tax in self.get("taxes"): - validate_taxes_and_charges(tax) - validate_inclusive_tax(tax, self) - - tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] - - if tax.charge_type != "Actual": - tax_fields.append("tax_amount") - - for fieldname in tax_fields: - tax.set(fieldname, 0.0) - - self.paid_amount_after_tax = self.paid_amount - - def determine_exclusive_rate(self): - if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")): - return - - cumulated_tax_fraction = 0 - for i, tax in enumerate(self.get("taxes")): - tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax) - if i == 0: - tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item - else: - tax.grand_total_fraction_for_current_item = ( - self.get("taxes")[i - 1].grand_total_fraction_for_current_item - + tax.tax_fraction_for_current_item - ) - - cumulated_tax_fraction += tax.tax_fraction_for_current_item - - self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction)) - - def calculate_taxes(self): - self.total_taxes_and_charges = 0.0 - self.base_total_taxes_and_charges = 0.0 - - actual_tax_dict = dict( - [ - [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] - for tax in self.get("taxes") - if tax.charge_type == "Actual" - ] - ) - - for i, tax in enumerate(self.get("taxes")): - current_tax_amount = self.get_current_tax_amount(tax) - - if tax.charge_type == "Actual": - actual_tax_dict[tax.idx] -= current_tax_amount - if i == len(self.get("taxes")) - 1: - current_tax_amount += actual_tax_dict[tax.idx] - - tax.tax_amount = current_tax_amount - tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate - - if tax.add_deduct_tax == "Deduct": - current_tax_amount *= -1.0 - else: - current_tax_amount *= 1.0 - - if i == 0: - tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax)) - else: - tax.total = flt( - self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax) - ) - - tax.base_total = tax.total * self.source_exchange_rate - - if self.payment_type == "Pay": - self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - else: - self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) - self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) - - if self.get("taxes"): - self.paid_amount_after_tax = self.get("taxes")[-1].base_total - - def get_current_tax_amount(self, tax): - tax_rate = tax.rate - - # To set row_id by default as previous row. - if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]: - if tax.idx == 1: - frappe.throw( - _( - "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" - ) - ) - - if not tax.row_id: - tax.row_id = tax.idx - 1 - - if tax.charge_type == "Actual": - current_tax_amount = flt(tax.tax_amount, self.precision("tax_amount", tax)) - elif tax.charge_type == "On Paid Amount": - current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax - elif tax.charge_type == "On Previous Row Amount": - current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].tax_amount - - elif tax.charge_type == "On Previous Row Total": - current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].total - - return current_tax_amount - - def get_current_tax_fraction(self, tax): - current_tax_fraction = 0 - - if cint(tax.included_in_paid_amount): - tax_rate = tax.rate - - if tax.charge_type == "On Paid Amount": - current_tax_fraction = tax_rate / 100.0 - elif tax.charge_type == "On Previous Row Amount": - current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[ - cint(tax.row_id) - 1 - ].tax_fraction_for_current_item - elif tax.charge_type == "On Previous Row Total": - current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[ - cint(tax.row_id) - 1 - ].grand_total_fraction_for_current_item - - if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct": - current_tax_fraction *= -1.0 - - return current_tax_fraction + def __init__(self, *args, **kwargs): + super(PaymentEntry, self).__init__(*args, **kwargs) + if not self.is_new(): + self.setup_party_account_field() + + def setup_party_account_field(self): + self.party_account_field = None + self.party_account = None + self.party_account_currency = None + + if self.payment_type == "Receive": + self.party_account_field = "paid_from" + self.party_account = self.paid_from + self.party_account_currency = self.paid_from_account_currency + + elif self.payment_type == "Pay": + self.party_account_field = "paid_to" + self.party_account = self.paid_to + self.party_account_currency = self.paid_to_account_currency + + def validate(self): + self.setup_party_account_field() + self.set_missing_values() + self.validate_payment_type() + self.validate_party_details() + self.validate_bank_accounts() + self.set_exchange_rate() + self.validate_mandatory() + self.validate_reference_documents() + self.set_tax_withholding() + self.set_amounts() + self.validate_amounts() + self.apply_taxes() + self.set_amounts_after_tax() + self.clear_unallocated_reference_document_rows() + self.validate_payment_against_negative_invoice() + self.validate_transaction_reference() + self.set_title() + self.set_remarks() + self.validate_duplicate_entry() + self.validate_payment_type_with_outstanding() + self.validate_allocated_amount() + self.validate_paid_invoices() + self.ensure_supplier_is_not_blocked() + self.set_status() + + def on_submit(self): + if self.difference_amount: + frappe.throw(_("Difference Amount must be zero")) + self.make_gl_entries() + self.update_expense_claim() + self.update_outstanding_amounts() + self.update_advance_paid() + self.update_payment_schedule() + self.set_status() + + def on_cancel(self): + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.make_gl_entries(cancel=1) + self.update_expense_claim() + self.update_outstanding_amounts() + self.update_advance_paid() + self.delink_advance_entry_references() + self.update_payment_schedule(cancel=1) + self.set_payment_req_status() + self.set_status() + + def set_payment_req_status(self): + from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status + + update_payment_req_status(self, None) + + def update_outstanding_amounts(self): + self.set_missing_ref_details(force=True) + + def validate_duplicate_entry(self): + reference_names = [] + for d in self.get("references"): + if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: + frappe.throw( + _("Row #{0}: Duplicate entry in References {1} {2}").format( + d.idx, d.reference_doctype, d.reference_name + ) + ) + reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) + + def set_bank_account_data(self): + if self.bank_account: + bank_data = get_bank_account_details(self.bank_account) + + field = "paid_from" if self.payment_type == "Pay" else "paid_to" + + self.bank = bank_data.bank + self.bank_account_no = bank_data.bank_account_no + + if not self.get(field): + self.set(field, bank_data.account) + + def validate_payment_type_with_outstanding(self): + total_outstanding = sum(d.allocated_amount for d in self.get("references")) + if total_outstanding < 0 and self.party_type == "Customer" and self.payment_type == "Receive": + frappe.throw( + _("Cannot receive from customer against negative outstanding"), + title=_("Incorrect Payment Type"), + ) + + def validate_allocated_amount(self): + for d in self.get("references"): + if (flt(d.allocated_amount)) > 0: + if flt(d.allocated_amount) > flt(d.outstanding_amount): + frappe.throw( + _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx) + ) + + # Check for negative outstanding invoices as well + if flt(d.allocated_amount) < 0: + if flt(d.allocated_amount) < flt(d.outstanding_amount): + frappe.throw( + _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx) + ) + + def delink_advance_entry_references(self): + for reference in self.references: + if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"): + doc = frappe.get_doc(reference.reference_doctype, reference.reference_name) + doc.delink_advance_entries(self.name) + + def set_missing_values(self): + if self.payment_type == "Internal Transfer": + for field in ( + "party", + "party_balance", + "total_allocated_amount", + "base_total_allocated_amount", + "unallocated_amount", + ): + self.set(field, None) + self.references = [] + else: + if not self.party_type: + frappe.throw(_("Party Type is mandatory")) + + if not self.party: + frappe.throw(_("Party is mandatory")) + + _party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name" + self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name) + + if self.party: + if not self.party_balance: + self.party_balance = get_balance_on( + party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company + ) + + if not self.party_account: + party_account = get_party_account(self.party_type, self.party, self.company) + self.set(self.party_account_field, party_account) + self.party_account = party_account + + if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance): + acc = get_account_details(self.paid_from, self.posting_date, self.cost_center) + self.paid_from_account_currency = acc.account_currency + self.paid_from_account_balance = acc.account_balance + + if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance): + acc = get_account_details(self.paid_to, self.posting_date, self.cost_center) + self.paid_to_account_currency = acc.account_currency + self.paid_to_account_balance = acc.account_balance + + self.party_account_currency = ( + self.paid_from_account_currency + if self.payment_type == "Receive" + else self.paid_to_account_currency + ) + + self.set_missing_ref_details() + + def set_missing_ref_details(self, force=False): + for d in self.get("references"): + if d.allocated_amount: + ref_details = get_reference_details( + d.reference_doctype, d.reference_name, self.party_account_currency + ) + + for field, value in ref_details.items(): + if d.exchange_gain_loss: + # for cases where gain/loss is booked into invoice + # exchange_gain_loss is calculated from invoice & populated + # and row.exchange_rate is already set to payment entry's exchange rate + # refer -> `update_reference_in_payment_entry()` in utils.py + continue + + if field == "exchange_rate" or not d.get(field) or force: + d.db_set(field, value) + + def validate_payment_type(self): + if self.payment_type not in ("Receive", "Pay", "Internal Transfer"): + frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer")) + + def validate_party_details(self): + if self.party: + if not frappe.db.exists(self.party_type, self.party): + frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party)) + + if self.party_account and self.party_type in ("Customer", "Supplier"): + self.validate_account_type( + self.party_account, [erpnext.get_party_account_type(self.party_type)] + ) + + def validate_bank_accounts(self): + if self.payment_type in ("Pay", "Internal Transfer"): + self.validate_account_type(self.paid_from, ["Bank", "Cash"]) + + if self.payment_type in ("Receive", "Internal Transfer"): + self.validate_account_type(self.paid_to, ["Bank", "Cash"]) + + def validate_account_type(self, account, account_types): + account_type = frappe.db.get_value("Account", account, "account_type") + # if account_type not in account_types: + # frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) + + def set_exchange_rate(self, ref_doc=None): + self.set_source_exchange_rate(ref_doc) + self.set_target_exchange_rate(ref_doc) + + def set_source_exchange_rate(self, ref_doc=None): + if self.paid_from and not self.source_exchange_rate: + if self.paid_from_account_currency == self.company_currency: + self.source_exchange_rate = 1 + else: + if ref_doc: + if self.paid_from_account_currency == ref_doc.currency: + self.source_exchange_rate = ref_doc.get("exchange_rate") + + if not self.source_exchange_rate: + self.source_exchange_rate = get_exchange_rate( + self.paid_from_account_currency, self.company_currency, self.posting_date + ) + + def set_target_exchange_rate(self, ref_doc=None): + if self.paid_from_account_currency == self.paid_to_account_currency: + self.target_exchange_rate = self.source_exchange_rate + elif self.paid_to and not self.target_exchange_rate: + if ref_doc: + if self.paid_to_account_currency == ref_doc.currency: + self.target_exchange_rate = ref_doc.get("exchange_rate") + + if not self.target_exchange_rate: + self.target_exchange_rate = get_exchange_rate( + self.paid_to_account_currency, self.company_currency, self.posting_date + ) + + def validate_mandatory(self): + for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): + if not self.get(field): + frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field))) + + def validate_reference_documents(self): + if self.party_type == "Customer": + valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") + elif self.party_type == "Supplier": + valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") + elif self.party_type == "Employee": + valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity", "Sales Commission") + elif self.party_type == "Shareholder": + valid_reference_doctypes = "Journal Entry" + + for d in self.get("references"): + if not d.allocated_amount: + continue + if d.reference_doctype not in valid_reference_doctypes: + frappe.throw( + _("Reference Doctype must be one of {0}").format(comma_or(valid_reference_doctypes)) + ) + + elif d.reference_name: + if not frappe.db.exists(d.reference_doctype, d.reference_name): + frappe.throw(_("{0} {1} does not exist").format(d.reference_doctype, d.reference_name)) + else: + ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name) + + if d.reference_doctype != "Journal Entry": + if self.party != ref_doc.get(scrub(self.party_type)): + frappe.throw( + _("{0} {1} is not associated with {2} {3}").format( + d.reference_doctype, d.reference_name, self.party_type, self.party + ) + ) + else: + self.validate_journal_entry() + + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Expense Claim", "Fees"): + if self.party_type == "Customer": + ref_party_account = ( + get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to + ) + elif self.party_type == "Supplier": + ref_party_account = ref_doc.credit_to + elif self.party_type == "Employee": + ref_party_account = ref_doc.payable_account + + if ref_party_account != self.party_account: + frappe.throw( + _("{0} {1} is associated with {2}, but Party Account is {3}").format( + d.reference_doctype, d.reference_name, ref_party_account, self.party_account + ) + ) + + if ref_doc.docstatus != 1: + frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name)) + + def validate_paid_invoices(self): + no_oustanding_refs = {} + + for d in self.get("references"): + if not d.allocated_amount: + continue + + if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"): + outstanding_amount, is_return = frappe.get_cached_value( + d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"] + ) + if outstanding_amount <= 0 and not is_return: + no_oustanding_refs.setdefault(d.reference_doctype, []).append(d) + + for k, v in no_oustanding_refs.items(): + frappe.msgprint( + _( + "{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry." + ).format( + _(k), + frappe.bold(", ".join(d.reference_name for d in v)), + frappe.bold(_("negative outstanding amount")), + ) + + "

" + + _("If this is undesirable please cancel the corresponding Payment Entry."), + title=_("Warning"), + indicator="orange", + ) + + def validate_journal_entry(self): + for d in self.get("references"): + if d.allocated_amount and d.reference_doctype == "Journal Entry": + je_accounts = frappe.db.sql( + """select debit, credit from `tabJournal Entry Account` + where account = %s and party=%s and docstatus = 1 and parent = %s + and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order")) + """, + (self.party_account, self.party, d.reference_name), + as_dict=True, + ) + + if not je_accounts: + frappe.throw( + _( + "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher" + ).format(d.idx, d.reference_name, self.party_account) + ) + else: + dr_or_cr = "debit" if self.payment_type == "Receive" else "credit" + valid = False + for jvd in je_accounts: + if flt(jvd[dr_or_cr]) > 0: + valid = True + if not valid: + frappe.throw( + _("Against Journal Entry {0} does not have any unmatched {1} entry").format( + d.reference_name, dr_or_cr + ) + ) + + def update_payment_schedule(self, cancel=0): + invoice_payment_amount_map = {} + invoice_paid_amount_map = {} + + for ref in self.get("references"): + if ref.payment_term and ref.reference_name: + key = (ref.payment_term, ref.reference_name) + invoice_payment_amount_map.setdefault(key, 0.0) + invoice_payment_amount_map[key] += ref.allocated_amount + + if not invoice_paid_amount_map.get(key): + payment_schedule = frappe.get_all( + "Payment Schedule", + filters={"parent": ref.reference_name}, + fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"], + ) + for term in payment_schedule: + invoice_key = (term.payment_term, ref.reference_name) + invoice_paid_amount_map.setdefault(invoice_key, {}) + invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding + invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * ( + term.discount / 100 + ) + + for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1): + if not invoice_paid_amount_map.get(key): + frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1])) + + outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding")) + discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt")) + + if cancel: + frappe.db.sql( + """ + UPDATE `tabPayment Schedule` + SET + paid_amount = `paid_amount` - %s, + discounted_amount = `discounted_amount` - %s, + outstanding = `outstanding` + %s + WHERE parent = %s and payment_term = %s""", + (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), + ) + else: + if allocated_amount > outstanding: + frappe.throw( + _("Row #{0}: Cannot allocate more than {1} against payment term {2}").format( + idx, outstanding, key[0] + ) + ) + + if allocated_amount and outstanding: + frappe.db.sql( + """ + UPDATE `tabPayment Schedule` + SET + paid_amount = `paid_amount` + %s, + discounted_amount = `discounted_amount` + %s, + outstanding = `outstanding` - %s + WHERE parent = %s and payment_term = %s""", + (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]), + ) + + def set_status(self): + if self.docstatus == 2: + self.status = "Cancelled" + elif self.docstatus == 1: + self.status = "Submitted" + else: + self.status = "Draft" + + self.db_set("status", self.status, update_modified=True) + + def set_tax_withholding(self): + if not self.party_type == "Supplier": + return + + if not self.apply_tax_withholding_amount: + return + + net_total = self.paid_amount + + # Adding args as purchase invoice to get TDS amount + args = frappe._dict( + { + "company": self.company, + "doctype": "Payment Entry", + "supplier": self.party, + "posting_date": self.posting_date, + "net_total": net_total, + } + ) + + tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category) + + if not tax_withholding_details: + return + + tax_withholding_details.update( + {"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)} + ) + + accounts = [] + for d in self.taxes: + if d.account_head == tax_withholding_details.get("account_head"): + + # Preserve user updated included in paid amount + if d.included_in_paid_amount: + tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount}) + + d.update(tax_withholding_details) + accounts.append(d.account_head) + + if not accounts or tax_withholding_details.get("account_head") not in accounts: + self.append("taxes", tax_withholding_details) + + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head") + ] + + for d in to_remove: + self.remove(d) + + def apply_taxes(self): + self.initialize_taxes() + self.determine_exclusive_rate() + self.calculate_taxes() + + def set_amounts(self): + self.set_received_amount() + self.set_amounts_in_company_currency() + self.set_total_allocated_amount() + self.set_unallocated_amount() + self.set_difference_amount() + + def validate_amounts(self): + self.validate_received_amount() + + def validate_received_amount(self): + if self.paid_from_account_currency == self.paid_to_account_currency: + if self.paid_amount < self.received_amount: + frappe.throw(_("Received Amount cannot be greater than Paid Amount")) + + def set_received_amount(self): + self.base_received_amount = self.base_paid_amount + if ( + self.paid_from_account_currency == self.paid_to_account_currency + and not self.payment_type == "Internal Transfer" + ): + self.received_amount = self.paid_amount + + def set_amounts_after_tax(self): + applicable_tax = 0 + base_applicable_tax = 0 + for tax in self.get("taxes"): + if not tax.included_in_paid_amount: + amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount + base_amount = ( + -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount + ) + + applicable_tax += amount + base_applicable_tax += base_amount + + self.paid_amount_after_tax = flt( + flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") + ) + self.base_paid_amount_after_tax = flt( + flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate), + self.precision("base_paid_amount_after_tax"), + ) + + self.received_amount_after_tax = flt( + flt(self.received_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax") + ) + self.base_received_amount_after_tax = flt( + flt(self.received_amount_after_tax) * flt(self.target_exchange_rate), + self.precision("base_paid_amount_after_tax"), + ) + + def set_amounts_in_company_currency(self): + self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0 + if self.paid_amount: + self.base_paid_amount = flt( + flt(self.paid_amount) * flt(self.source_exchange_rate), self.precision("base_paid_amount") + ) + + if self.received_amount: + self.base_received_amount = flt( + flt(self.received_amount) * flt(self.target_exchange_rate), + self.precision("base_received_amount"), + ) + + def set_total_allocated_amount(self): + if self.payment_type == "Internal Transfer": + return + + total_allocated_amount, base_total_allocated_amount = 0, 0 + for d in self.get("references"): + if d.allocated_amount: + total_allocated_amount += flt(d.allocated_amount) + base_total_allocated_amount += flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + ) + + self.total_allocated_amount = abs(total_allocated_amount) + self.base_total_allocated_amount = abs(base_total_allocated_amount) + + def set_unallocated_amount(self): + self.unallocated_amount = 0 + if self.party: + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() + if ( + self.payment_type == "Receive" + and self.base_total_allocated_amount < self.base_received_amount + total_deductions + and self.total_allocated_amount + < self.paid_amount + (total_deductions / self.source_exchange_rate) + ): + self.unallocated_amount = ( + self.base_received_amount + total_deductions - self.base_total_allocated_amount + ) / self.source_exchange_rate + self.unallocated_amount -= included_taxes + elif ( + self.payment_type == "Pay" + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) + and self.total_allocated_amount + < self.received_amount + (total_deductions / self.target_exchange_rate) + ): + self.unallocated_amount = ( + self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) + ) / self.target_exchange_rate + self.unallocated_amount -= included_taxes + + def set_difference_amount(self): + base_unallocated_amount = flt(self.unallocated_amount) * ( + flt(self.source_exchange_rate) + if self.payment_type == "Receive" + else flt(self.target_exchange_rate) + ) + + base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) + + if self.payment_type == "Receive": + self.difference_amount = base_party_amount - self.base_received_amount + elif self.payment_type == "Pay": + self.difference_amount = self.base_paid_amount - base_party_amount + else: + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) + + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() + + self.difference_amount = flt( + self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") + ) + + def get_included_taxes(self): + included_taxes = 0 + for tax in self.get("taxes"): + if tax.included_in_paid_amount: + if tax.add_deduct_tax == "Add": + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount + + return included_taxes + + # Paid amount is auto allocated in the reference document by default. + # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast + def clear_unallocated_reference_document_rows(self): + self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]})) + frappe.db.sql( + """delete from `tabPayment Entry Reference` + where parent = %s and allocated_amount = 0""", + self.name, + ) + + def validate_payment_against_negative_invoice(self): + if (self.payment_type == "Pay" and self.party_type == "Customer") or ( + self.payment_type == "Receive" and self.party_type == "Supplier" + ): + + total_negative_outstanding = sum( + abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 + ) + + paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount + additional_charges = sum([flt(d.amount) for d in self.deductions]) + + if not total_negative_outstanding: + frappe.throw( + _("Cannot {0} {1} {2} without any negative outstanding invoice").format( + _(self.payment_type), + (_("to") if self.party_type == "Customer" else _("from")), + self.party_type, + ), + InvalidPaymentEntry, + ) + + elif paid_amount - additional_charges > total_negative_outstanding: + frappe.throw( + _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( + total_negative_outstanding + ), + InvalidPaymentEntry, + ) + + def set_title(self): + if frappe.flags.in_import and self.title: + # do not set title dynamically if title exists during data import. + return + + if self.payment_type in ("Receive", "Pay"): + self.title = self.party + else: + self.title = self.paid_from + " - " + self.paid_to + + def validate_transaction_reference(self): + bank_account = self.paid_to if self.payment_type == "Receive" else self.paid_from + bank_account_type = frappe.db.get_value("Account", bank_account, "account_type") + + if bank_account_type == "Bank": + if not self.reference_no or not self.reference_date: + frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) + + def set_remarks(self): + if self.custom_remarks: + return + + if self.payment_type == "Internal Transfer": + remarks = [ + _("Amount {0} {1} transferred from {2} to {3}").format( + self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to + ) + ] + else: + + remarks = [ + _("Amount {0} {1} {2} {3}").format( + self.party_account_currency, + self.paid_amount if self.payment_type == "Receive" else self.received_amount, + _("received from") if self.payment_type == "Receive" else _("to"), + self.party, + ) + ] + + if self.reference_no: + remarks.append( + _("Transaction reference no {0} dated {1}").format(self.reference_no, self.reference_date) + ) + + if self.payment_type in ["Receive", "Pay"]: + for d in self.get("references"): + if d.allocated_amount: + remarks.append( + _("Amount {0} {1} against {2} {3}").format( + self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name + ) + ) + + for d in self.get("deductions"): + if d.amount: + remarks.append( + _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account) + ) + + self.set("remarks", "\n".join(remarks)) + + def make_gl_entries(self, cancel=0, adv_adj=0): + if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"): + self.setup_party_account_field() + + gl_entries = [] + self.add_party_gl_entries(gl_entries) + self.add_bank_gl_entries(gl_entries) + self.add_deductions_gl_entries(gl_entries) + self.add_tax_gl_entries(gl_entries) + + gl_entries = process_gl_map(gl_entries) + make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) + + def add_party_gl_entries(self, gl_entries): + if self.party_account: + if self.payment_type == "Receive": + against_account = self.paid_to + else: + against_account = self.paid_from + + party_gl_dict = self.get_gl_dict( + { + "account": self.party_account, + "party_type": self.party_type, + "party": self.party, + "against": against_account, + "account_currency": self.party_account_currency, + "cost_center": self.cost_center, + }, + item=self, + ) + + dr_or_cr = ( + "credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit" + ) + + for d in self.get("references"): + cost_center = self.cost_center + if d.reference_doctype == "Sales Invoice" and not cost_center: + cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") + gle = party_gl_dict.copy() + gle.update( + { + "against_voucher_type": d.reference_doctype, + "against_voucher": d.reference_name, + "cost_center": cost_center, + } + ) + + allocated_amount_in_company_currency = flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount") + ) + + gle.update( + { + dr_or_cr + "_in_account_currency": d.allocated_amount, + dr_or_cr: allocated_amount_in_company_currency, + } + ) + + gl_entries.append(gle) + + if self.unallocated_amount: + exchange_rate = self.get_exchange_rate() + base_unallocated_amount = self.unallocated_amount * exchange_rate + + gle = party_gl_dict.copy() + + gle.update( + { + dr_or_cr + "_in_account_currency": self.unallocated_amount, + dr_or_cr: base_unallocated_amount, + } + ) + + gl_entries.append(gle) + + def add_bank_gl_entries(self, gl_entries): + if self.payment_type in ("Pay", "Internal Transfer"): + gl_entries.append( + self.get_gl_dict( + { + "account": self.paid_from, + "account_currency": self.paid_from_account_currency, + "against": self.party if self.payment_type == "Pay" else self.paid_to, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, + "cost_center": self.cost_center, + "post_net_value": True, + }, + item=self, + ) + ) + if self.payment_type in ("Receive", "Internal Transfer"): + gl_entries.append( + self.get_gl_dict( + { + "account": self.paid_to, + "account_currency": self.paid_to_account_currency, + "against": self.party if self.payment_type == "Receive" else self.paid_from, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, + "cost_center": self.cost_center, + }, + item=self, + ) + ) + + def add_tax_gl_entries(self, gl_entries): + for d in self.get("taxes"): + account_currency = get_account_currency(d.account_head) + if account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) + + if self.payment_type in ("Pay", "Internal Transfer"): + dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" + rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" + against = self.party or self.paid_from + elif self.payment_type == "Receive": + dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" + rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" + against = self.party or self.paid_to + + payment_account = self.get_party_account_for_taxes() + tax_amount = d.tax_amount + base_tax_amount = d.base_tax_amount + + gl_entries.append( + self.get_gl_dict( + { + "account": d.account_head, + "against": against, + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount + if account_currency == self.company_currency + else d.tax_amount, + "cost_center": d.cost_center, + "post_net_value": True, + }, + account_currency, + item=d, + ) + ) + + if not d.included_in_paid_amount: + gl_entries.append( + self.get_gl_dict( + { + "account": payment_account, + "against": against, + rev_dr_or_cr: tax_amount, + rev_dr_or_cr + "_in_account_currency": base_tax_amount + if account_currency == self.company_currency + else d.tax_amount, + "cost_center": self.cost_center, + "post_net_value": True, + }, + account_currency, + item=d, + ) + ) + + def add_deductions_gl_entries(self, gl_entries): + for d in self.get("deductions"): + if d.amount: + account_currency = get_account_currency(d.account) + if account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) + + gl_entries.append( + self.get_gl_dict( + { + "account": d.account, + "account_currency": account_currency, + "against": self.party or self.paid_from, + "debit_in_account_currency": d.amount, + "debit": d.amount, + "cost_center": d.cost_center, + }, + item=d, + ) + ) + + def get_party_account_for_taxes(self): + if self.payment_type == "Receive": + return self.paid_to + elif self.payment_type in ("Pay", "Internal Transfer"): + return self.paid_from + + def update_advance_paid(self): + if self.payment_type in ("Receive", "Pay") and self.party: + for d in self.get("references"): + if d.allocated_amount and d.reference_doctype in ( + "Sales Order", + "Purchase Order", + "Employee Advance", + "Gratuity", + ): + frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() + + def update_expense_claim(self): + if self.payment_type in ("Pay") and self.party: + for d in self.get("references"): + if d.reference_doctype == "Expense Claim" and d.reference_name: + doc = frappe.get_doc("Expense Claim", d.reference_name) + if self.docstatus == 2: + update_reimbursed_amount(doc, -1 * d.allocated_amount) + else: + update_reimbursed_amount(doc, d.allocated_amount) + + def on_recurring(self, reference_doc, auto_repeat_doc): + self.reference_no = reference_doc.name + self.reference_date = nowdate() + + def calculate_deductions(self, tax_details): + return { + "account": tax_details["tax"]["account_head"], + "cost_center": frappe.get_cached_value("Company", self.company, "cost_center"), + "amount": self.total_allocated_amount * (tax_details["tax"]["rate"] / 100), + } + + def set_gain_or_loss(self, account_details=None): + if not self.difference_amount: + self.set_difference_amount() + + row = {"amount": self.difference_amount} + + if account_details: + row.update(account_details) + + if not row.get("amount"): + # if no difference amount + return + + self.append("deductions", row) + self.set_unallocated_amount() + + def get_exchange_rate(self): + return self.source_exchange_rate if self.payment_type == "Receive" else self.target_exchange_rate + + def initialize_taxes(self): + for tax in self.get("taxes"): + validate_taxes_and_charges(tax) + validate_inclusive_tax(tax, self) + + tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] + + if tax.charge_type != "Actual": + tax_fields.append("tax_amount") + + for fieldname in tax_fields: + tax.set(fieldname, 0.0) + + self.paid_amount_after_tax = self.paid_amount + + def determine_exclusive_rate(self): + if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")): + return + + cumulated_tax_fraction = 0 + for i, tax in enumerate(self.get("taxes")): + tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax) + if i == 0: + tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item + else: + tax.grand_total_fraction_for_current_item = ( + self.get("taxes")[i - 1].grand_total_fraction_for_current_item + + tax.tax_fraction_for_current_item + ) + + cumulated_tax_fraction += tax.tax_fraction_for_current_item + + self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction)) + + def calculate_taxes(self): + self.total_taxes_and_charges = 0.0 + self.base_total_taxes_and_charges = 0.0 + + actual_tax_dict = dict( + [ + [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] + for tax in self.get("taxes") + if tax.charge_type == "Actual" + ] + ) + + for i, tax in enumerate(self.get("taxes")): + current_tax_amount = self.get_current_tax_amount(tax) + + if tax.charge_type == "Actual": + actual_tax_dict[tax.idx] -= current_tax_amount + if i == len(self.get("taxes")) - 1: + current_tax_amount += actual_tax_dict[tax.idx] + + tax.tax_amount = current_tax_amount + tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate + + if tax.add_deduct_tax == "Deduct": + current_tax_amount *= -1.0 + else: + current_tax_amount *= 1.0 + + if i == 0: + tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax)) + else: + tax.total = flt( + self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax) + ) + + tax.base_total = tax.total * self.source_exchange_rate + + if self.payment_type == "Pay": + self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + else: + self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + + if self.get("taxes"): + self.paid_amount_after_tax = self.get("taxes")[-1].base_total + + def get_current_tax_amount(self, tax): + tax_rate = tax.rate + + # To set row_id by default as previous row. + if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]: + if tax.idx == 1: + frappe.throw( + _( + "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row" + ) + ) + + if not tax.row_id: + tax.row_id = tax.idx - 1 + + if tax.charge_type == "Actual": + current_tax_amount = flt(tax.tax_amount, self.precision("tax_amount", tax)) + elif tax.charge_type == "On Paid Amount": + current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax + elif tax.charge_type == "On Previous Row Amount": + current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].tax_amount + + elif tax.charge_type == "On Previous Row Total": + current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].total + + return current_tax_amount + + def get_current_tax_fraction(self, tax): + current_tax_fraction = 0 + + if cint(tax.included_in_paid_amount): + tax_rate = tax.rate + + if tax.charge_type == "On Paid Amount": + current_tax_fraction = tax_rate / 100.0 + elif tax.charge_type == "On Previous Row Amount": + current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[ + cint(tax.row_id) - 1 + ].tax_fraction_for_current_item + elif tax.charge_type == "On Previous Row Total": + current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[ + cint(tax.row_id) - 1 + ].grand_total_fraction_for_current_item + + if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct": + current_tax_fraction *= -1.0 + + return current_tax_fraction def validate_inclusive_tax(tax, doc): - def _on_previous_row_error(row_range): - throw( - _("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format( - tax.idx, row_range - ) - ) + def _on_previous_row_error(row_range): + throw( + _("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format( + tax.idx, row_range + ) + ) - if cint(getattr(tax, "included_in_paid_amount", None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw( - _("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format( - tax.idx - ) - ) - elif tax.charge_type == "On Previous Row Amount" and not cint( - doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount - ): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and not all( - [cint(t.included_in_paid_amount for t in doc.get("taxes")[: cint(tax.row_id) - 1])] - ): - # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + if cint(getattr(tax, "included_in_paid_amount", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw( + _("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format( + tax.idx + ) + ) + elif tax.charge_type == "On Previous Row Amount" and not cint( + doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount + ): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and not all( + [cint(t.included_in_paid_amount for t in doc.get("taxes")[: cint(tax.row_id) - 1])] + ): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) @frappe.whitelist() def get_outstanding_reference_documents(args): - if isinstance(args, str): - args = json.loads(args) + if isinstance(args, str): + args = json.loads(args) - if args.get("party_type") == "Member": - return + if args.get("party_type") == "Member": + return - # confirm that Supplier is not blocked - if args.get("party_type") == "Supplier": - supplier_status = get_supplier_block_status(args["party"]) - if supplier_status["on_hold"]: - if supplier_status["hold_type"] == "All": - return [] - elif supplier_status["hold_type"] == "Payments": - if ( - not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"] - ): - return [] + # confirm that Supplier is not blocked + if args.get("party_type") == "Supplier": + supplier_status = get_supplier_block_status(args["party"]) + if supplier_status["on_hold"]: + if supplier_status["hold_type"] == "All": + return [] + elif supplier_status["hold_type"] == "Payments": + if ( + not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"] + ): + return [] - party_account_currency = get_account_currency(args.get("party_account")) - company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") + party_account_currency = get_account_currency(args.get("party_account")) + company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") - # Get positive outstanding sales /purchase invoices/ Fees - condition = "" - if args.get("voucher_type") and args.get("voucher_no"): - condition = " and voucher_type={0} and voucher_no={1}".format( - frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) - ) + # Get positive outstanding sales /purchase invoices/ Fees + condition = "" + if args.get("voucher_type") and args.get("voucher_no"): + condition = " and voucher_type={0} and voucher_no={1}".format( + frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) + ) - # Add cost center condition - if args.get("cost_center"): - condition += " and cost_center='%s'" % args.get("cost_center") + # Add cost center condition + if args.get("cost_center"): + condition += " and cost_center='%s'" % args.get("cost_center") - date_fields_dict = { - "posting_date": ["from_posting_date", "to_posting_date"], - "due_date": ["from_due_date", "to_due_date"], - } + date_fields_dict = { + "posting_date": ["from_posting_date", "to_posting_date"], + "due_date": ["from_due_date", "to_due_date"], + } - for fieldname, date_fields in date_fields_dict.items(): - if args.get(date_fields[0]) and args.get(date_fields[1]): - condition += " and {0} between '{1}' and '{2}'".format( - fieldname, args.get(date_fields[0]), args.get(date_fields[1]) - ) + for fieldname, date_fields in date_fields_dict.items(): + if args.get(date_fields[0]) and args.get(date_fields[1]): + condition += " and {0} between '{1}' and '{2}'".format( + fieldname, args.get(date_fields[0]), args.get(date_fields[1]) + ) - if args.get("company"): - condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) + if args.get("company"): + condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) - outstanding_invoices = get_outstanding_invoices( - args.get("party_type"), - args.get("party"), - args.get("party_account"), - filters=args, - condition=condition, - ) + outstanding_invoices = get_outstanding_invoices( + args.get("party_type"), + args.get("party"), + args.get("party_account"), + filters=args, + condition=condition, + ) - outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) + outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) - for d in outstanding_invoices: - d["exchange_rate"] = 1 - if party_account_currency != company_currency: - if d.voucher_type in ("Sales Invoice", "Purchase Invoice", "Expense Claim"): - d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate") - elif d.voucher_type == "Journal Entry": - d["exchange_rate"] = get_exchange_rate( - party_account_currency, company_currency, d.posting_date - ) - if d.voucher_type in ("Purchase Invoice"): - d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") + for d in outstanding_invoices: + d["exchange_rate"] = 1 + if party_account_currency != company_currency: + if d.voucher_type in ("Sales Invoice", "Purchase Invoice", "Expense Claim"): + d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate") + elif d.voucher_type == "Journal Entry": + d["exchange_rate"] = get_exchange_rate( + party_account_currency, company_currency, d.posting_date + ) + if d.voucher_type in ("Purchase Invoice"): + d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") - # Get all SO / PO which are not fully billed or against which full advance not paid - orders_to_be_billed = [] - orders_to_be_billed = get_orders_to_be_billed( - args.get("posting_date"), - args.get("party_type"), - args.get("party"), - args.get("company"), - party_account_currency, - company_currency, - filters=args, - ) + # Get all SO / PO which are not fully billed or against which full advance not paid + orders_to_be_billed = [] + orders_to_be_billed = get_orders_to_be_billed( + args.get("posting_date"), + args.get("party_type"), + args.get("party"), + args.get("company"), + party_account_currency, + company_currency, + filters=args, + ) - # Get negative outstanding sales /purchase invoices - negative_outstanding_invoices = [] - if args.get("party_type") != "Employee" and not args.get("voucher_no"): - negative_outstanding_invoices = get_negative_outstanding_invoices( - args.get("party_type"), - args.get("party"), - args.get("party_account"), - party_account_currency, - company_currency, - condition=condition, - ) + # Get negative outstanding sales /purchase invoices + negative_outstanding_invoices = [] + if args.get("party_type") != "Employee" and not args.get("voucher_no"): + negative_outstanding_invoices = get_negative_outstanding_invoices( + args.get("party_type"), + args.get("party"), + args.get("party_account"), + party_account_currency, + company_currency, + condition=condition, + ) - data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed + data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed - if not data: - frappe.msgprint( - _( - "No outstanding invoices found for the {0} {1} which qualify the filters you have specified." - ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) - ) + if not data: + frappe.msgprint( + _( + "No outstanding invoices found for the {0} {1} which qualify the filters you have specified." + ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) + ) - return data + return data def split_invoices_based_on_payment_terms(outstanding_invoices): - invoice_ref_based_on_payment_terms = {} - for idx, d in enumerate(outstanding_invoices): - if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: - payment_term_template = frappe.db.get_value( - d.voucher_type, d.voucher_no, "payment_terms_template" - ) - if payment_term_template: - allocate_payment_based_on_payment_terms = frappe.db.get_value( - "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" - ) - if allocate_payment_based_on_payment_terms: - payment_schedule = frappe.get_all( - "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"] - ) + invoice_ref_based_on_payment_terms = {} + for idx, d in enumerate(outstanding_invoices): + if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + payment_term_template = frappe.db.get_value( + d.voucher_type, d.voucher_no, "payment_terms_template" + ) + if payment_term_template: + allocate_payment_based_on_payment_terms = frappe.db.get_value( + "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms" + ) + if allocate_payment_based_on_payment_terms: + payment_schedule = frappe.get_all( + "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"] + ) - for payment_term in payment_schedule: - if payment_term.outstanding > 0.1: - invoice_ref_based_on_payment_terms.setdefault(idx, []) - invoice_ref_based_on_payment_terms[idx].append( - frappe._dict( - { - "due_date": d.due_date, - "currency": d.currency, - "voucher_no": d.voucher_no, - "voucher_type": d.voucher_type, - "posting_date": d.posting_date, - "invoice_amount": flt(d.invoice_amount), - "outstanding_amount": flt(d.outstanding_amount), - "payment_amount": payment_term.payment_amount, - "payment_term": payment_term.payment_term, - } - ) - ) + for payment_term in payment_schedule: + if payment_term.outstanding > 0.1: + invoice_ref_based_on_payment_terms.setdefault(idx, []) + invoice_ref_based_on_payment_terms[idx].append( + frappe._dict( + { + "due_date": d.due_date, + "currency": d.currency, + "voucher_no": d.voucher_no, + "voucher_type": d.voucher_type, + "posting_date": d.posting_date, + "invoice_amount": flt(d.invoice_amount), + "outstanding_amount": flt(d.outstanding_amount), + "payment_amount": payment_term.payment_amount, + "payment_term": payment_term.payment_term, + } + ) + ) - outstanding_invoices_after_split = [] - if invoice_ref_based_on_payment_terms: - for idx, ref in invoice_ref_based_on_payment_terms.items(): - voucher_no = ref[0]["voucher_no"] - voucher_type = ref[0]["voucher_type"] + outstanding_invoices_after_split = [] + if invoice_ref_based_on_payment_terms: + for idx, ref in invoice_ref_based_on_payment_terms.items(): + voucher_no = ref[0]["voucher_no"] + voucher_type = ref[0]["voucher_type"] - frappe.msgprint( - _("Spliting {} {} into {} row(s) as per Payment Terms").format( - voucher_type, voucher_no, len(ref) - ), - alert=True, - ) + frappe.msgprint( + _("Spliting {} {} into {} row(s) as per Payment Terms").format( + voucher_type, voucher_no, len(ref) + ), + alert=True, + ) - outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx] + outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx] - existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices)) - index = outstanding_invoices.index(existing_row[0]) - outstanding_invoices.pop(index) + existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices)) + index = outstanding_invoices.index(existing_row[0]) + outstanding_invoices.pop(index) - outstanding_invoices_after_split += outstanding_invoices - return outstanding_invoices_after_split + outstanding_invoices_after_split += outstanding_invoices + return outstanding_invoices_after_split def get_orders_to_be_billed( - posting_date, - party_type, - party, - company, - party_account_currency, - company_currency, - cost_center=None, - filters=None, + posting_date, + party_type, + party, + company, + party_account_currency, + company_currency, + cost_center=None, + filters=None, ): - if party_type == "Customer": - voucher_type = "Sales Order" - elif party_type == "Supplier": - voucher_type = "Purchase Order" - elif party_type == "Employee": - voucher_type = None + if party_type == "Customer": + voucher_type = "Sales Order" + elif party_type == "Supplier": + voucher_type = "Purchase Order" + elif party_type == "Employee": + voucher_type = None - # Add cost center condition - if voucher_type: - doc = frappe.get_doc({"doctype": voucher_type}) - condition = "" - if doc and hasattr(doc, "cost_center"): - condition = " and cost_center='%s'" % cost_center + # Add cost center condition + if voucher_type: + doc = frappe.get_doc({"doctype": voucher_type}) + condition = "" + if doc and hasattr(doc, "cost_center"): + condition = " and cost_center='%s'" % cost_center - orders = [] - if voucher_type: - if party_account_currency == company_currency: - grand_total_field = "base_grand_total" - rounded_total_field = "base_rounded_total" - else: - grand_total_field = "grand_total" - rounded_total_field = "rounded_total" + orders = [] + if voucher_type: + if party_account_currency == company_currency: + grand_total_field = "base_grand_total" + rounded_total_field = "base_rounded_total" + else: + grand_total_field = "grand_total" + rounded_total_field = "rounded_total" - orders = frappe.db.sql( - """ - select - name as voucher_no, - if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, - (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, - transaction_date as posting_date - from - `tab{voucher_type}` - where - {party_type} = %s - and docstatus = 1 - and company = %s - and ifnull(status, "") != "Closed" - and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid - and abs(100 - per_billed) > 0.01 - {condition} - order by - transaction_date, name - """.format( - **{ - "rounded_total_field": rounded_total_field, - "grand_total_field": grand_total_field, - "voucher_type": voucher_type, - "party_type": scrub(party_type), - "condition": condition, - } - ), - (party, company), - as_dict=True, - ) + orders = frappe.db.sql( + """ + select + name as voucher_no, + if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, + (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, + transaction_date as posting_date + from + `tab{voucher_type}` + where + {party_type} = %s + and docstatus = 1 + and company = %s + and ifnull(status, "") != "Closed" + and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid + and abs(100 - per_billed) > 0.01 + {condition} + order by + transaction_date, name + """.format( + **{ + "rounded_total_field": rounded_total_field, + "grand_total_field": grand_total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "condition": condition, + } + ), + (party, company), + as_dict=True, + ) - order_list = [] - for d in orders: - if not ( - flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than")) - and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than")) - ): - continue + order_list = [] + for d in orders: + if not ( + flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than")) + and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than")) + ): + continue - d["voucher_type"] = voucher_type - # This assumes that the exchange rate required is the one in the SO - d["exchange_rate"] = get_exchange_rate(party_account_currency, company_currency, posting_date) - order_list.append(d) + d["voucher_type"] = voucher_type + # This assumes that the exchange rate required is the one in the SO + d["exchange_rate"] = get_exchange_rate(party_account_currency, company_currency, posting_date) + order_list.append(d) - return order_list + return order_list def get_negative_outstanding_invoices( - party_type, - party, - party_account, - party_account_currency, - company_currency, - cost_center=None, - condition=None, + party_type, + party, + party_account, + party_account_currency, + company_currency, + cost_center=None, + condition=None, ): - voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" - supplier_condition = "" - if voucher_type == "Purchase Invoice": - supplier_condition = "and (release_date is null or release_date <= CURDATE())" - if party_account_currency == company_currency: - grand_total_field = "base_grand_total" - rounded_total_field = "base_rounded_total" - else: - grand_total_field = "grand_total" - rounded_total_field = "rounded_total" + voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" + supplier_condition = "" + if voucher_type == "Purchase Invoice": + supplier_condition = "and (release_date is null or release_date <= CURDATE())" + if party_account_currency == company_currency: + grand_total_field = "base_grand_total" + rounded_total_field = "base_rounded_total" + else: + grand_total_field = "grand_total" + rounded_total_field = "rounded_total" - return frappe.db.sql( - """ - select - "{voucher_type}" as voucher_type, name as voucher_no, - if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, - outstanding_amount, posting_date, - due_date, conversion_rate as exchange_rate - from - `tab{voucher_type}` - where - {party_type} = %s and {party_account} = %s and docstatus = 1 and - outstanding_amount < 0 - {supplier_condition} - {condition} - order by - posting_date, name - """.format( - **{ - "supplier_condition": supplier_condition, - "condition": condition, - "rounded_total_field": rounded_total_field, - "grand_total_field": grand_total_field, - "voucher_type": voucher_type, - "party_type": scrub(party_type), - "party_account": "debit_to" if party_type == "Customer" else "credit_to", - "cost_center": cost_center, - } - ), - (party, party_account), - as_dict=True, - ) + return frappe.db.sql( + """ + select + "{voucher_type}" as voucher_type, name as voucher_no, + if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, + outstanding_amount, posting_date, + due_date, conversion_rate as exchange_rate + from + `tab{voucher_type}` + where + {party_type} = %s and {party_account} = %s and docstatus = 1 and + outstanding_amount < 0 + {supplier_condition} + {condition} + order by + posting_date, name + """.format( + **{ + "supplier_condition": supplier_condition, + "condition": condition, + "rounded_total_field": rounded_total_field, + "grand_total_field": grand_total_field, + "voucher_type": voucher_type, + "party_type": scrub(party_type), + "party_account": "debit_to" if party_type == "Customer" else "credit_to", + "cost_center": cost_center, + } + ), + (party, party_account), + as_dict=True, + ) @frappe.whitelist() def get_party_details(company, party_type, party, date, cost_center=None): - bank_account = "" - if not frappe.db.exists(party_type, party): - frappe.throw(_("Invalid {0}: {1}").format(party_type, party)) + bank_account = "" + if not frappe.db.exists(party_type, party): + frappe.throw(_("Invalid {0}: {1}").format(party_type, party)) - party_account = get_party_account(party_type, party, company) + party_account = get_party_account(party_type, party, company) - account_currency = get_account_currency(party_account) - account_balance = get_balance_on(party_account, date, cost_center=cost_center) - _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name" - party_name = frappe.db.get_value(party_type, party, _party_name) - party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) - if party_type in ["Customer", "Supplier"]: - bank_account = get_party_bank_account(party_type, party) + account_currency = get_account_currency(party_account) + account_balance = get_balance_on(party_account, date, cost_center=cost_center) + _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name" + party_name = frappe.db.get_value(party_type, party, _party_name) + party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) + if party_type in ["Customer", "Supplier"]: + bank_account = get_party_bank_account(party_type, party) - return { - "party_account": party_account, - "party_name": party_name, - "party_account_currency": account_currency, - "party_balance": party_balance, - "account_balance": account_balance, - "bank_account": bank_account, - } + return { + "party_account": party_account, + "party_name": party_name, + "party_account_currency": account_currency, + "party_balance": party_balance, + "account_balance": account_balance, + "bank_account": bank_account, + } @frappe.whitelist() def get_account_details(account, date, cost_center=None): - frappe.has_permission("Payment Entry", throw=True) + frappe.has_permission("Payment Entry", throw=True) - # to check if the passed account is accessible under reference doctype Payment Entry - account_list = frappe.get_list( - "Account", {"name": account}, reference_doctype="Payment Entry", limit=1 - ) + # to check if the passed account is accessible under reference doctype Payment Entry + account_list = frappe.get_list( + "Account", {"name": account}, reference_doctype="Payment Entry", limit=1 + ) - # There might be some user permissions which will allow account under certain doctypes - # except for Payment Entry, only in such case we should throw permission error - if not account_list: - frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account)) + # There might be some user permissions which will allow account under certain doctypes + # except for Payment Entry, only in such case we should throw permission error + if not account_list: + frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account)) - account_balance = get_balance_on( - account, date, cost_center=cost_center, ignore_account_permission=True - ) + account_balance = get_balance_on( + account, date, cost_center=cost_center, ignore_account_permission=True + ) - return frappe._dict( - { - "account_currency": get_account_currency(account), - "account_balance": account_balance, - "account_type": frappe.db.get_value("Account", account, "account_type"), - } - ) + return frappe._dict( + { + "account_currency": get_account_currency(account), + "account_balance": account_balance, + "account_type": frappe.db.get_value("Account", account, "account_type"), + } + ) @frappe.whitelist() def get_company_defaults(company): - fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"] - ret = frappe.get_cached_value("Company", company, fields, as_dict=1) + fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"] + ret = frappe.get_cached_value("Company", company, fields, as_dict=1) - for fieldname in fields: - if not ret[fieldname]: - frappe.throw( - _("Please set default {0} in Company {1}").format( - frappe.get_meta("Company").get_label(fieldname), company - ) - ) + for fieldname in fields: + if not ret[fieldname]: + frappe.throw( + _("Please set default {0} in Company {1}").format( + frappe.get_meta("Company").get_label(fieldname), company + ) + ) - return ret + return ret def get_outstanding_on_journal_entry(name): - res = frappe.db.sql( - "SELECT " - 'CASE WHEN party_type IN ("Customer") ' - "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) " - "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) " - "END as outstanding_amount " - "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) " - "AND party_type IS NOT NULL " - 'AND party_type != ""', - (name, name), - as_dict=1, - ) + res = frappe.db.sql( + "SELECT " + 'CASE WHEN party_type IN ("Customer") ' + "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) " + "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) " + "END as outstanding_amount " + "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) " + "AND party_type IS NOT NULL " + 'AND party_type != ""', + (name, name), + as_dict=1, + ) - outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0 + outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0 - return outstanding_amount + return outstanding_amount @frappe.whitelist() def get_reference_details(reference_doctype, reference_name, party_account_currency): - total_amount = outstanding_amount = exchange_rate = bill_no = None - ref_doc = frappe.get_doc(reference_doctype, reference_name) - company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency( - ref_doc.company - ) + total_amount = outstanding_amount = exchange_rate = bill_no = None + ref_doc = frappe.get_doc(reference_doctype, reference_name) + company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency( + ref_doc.company + ) - if reference_doctype == "Fees": - total_amount = ref_doc.get("grand_total") - exchange_rate = 1 - outstanding_amount = ref_doc.get("outstanding_amount") - elif reference_doctype == "Dunning": - total_amount = ref_doc.get("dunning_amount") - exchange_rate = 1 - outstanding_amount = ref_doc.get("dunning_amount") - elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: - total_amount = ref_doc.get("total_amount") - if ref_doc.multi_currency: - exchange_rate = get_exchange_rate( - party_account_currency, company_currency, ref_doc.posting_date - ) - else: - exchange_rate = 1 - outstanding_amount = get_outstanding_on_journal_entry(reference_name) - elif reference_doctype != "Journal Entry": - if ref_doc.doctype == "Expense Claim": - total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) - elif ref_doc.doctype == "Employee Advance": - total_amount = ref_doc.advance_amount - exchange_rate = ref_doc.get("exchange_rate") - if party_account_currency != ref_doc.currency: - total_amount = flt(total_amount) * flt(exchange_rate) - elif ref_doc.doctype == "Gratuity": - total_amount = ref_doc.amount - if not total_amount: - if party_account_currency == company_currency: - total_amount = ref_doc.base_grand_total - exchange_rate = 1 - else: - total_amount = ref_doc.grand_total - if not exchange_rate: - # Get the exchange rate from the original ref doc - # or get it based on the posting date of the ref doc. - exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate( - party_account_currency, company_currency, ref_doc.posting_date - ) - if reference_doctype in ("Sales Invoice", "Purchase Invoice"): - outstanding_amount = ref_doc.get("outstanding_amount") - bill_no = ref_doc.get("bill_no") - elif reference_doctype == "Expense Claim": - outstanding_amount = ( - flt(ref_doc.get("total_sanctioned_amount")) - + flt(ref_doc.get("total_taxes_and_charges")) - - flt(ref_doc.get("total_amount_reimbursed")) - - flt(ref_doc.get("total_advance_amount")) - ) - elif reference_doctype == "Employee Advance": - outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount) - if party_account_currency != ref_doc.currency: - outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) - if party_account_currency == company_currency: - exchange_rate = 1 - elif reference_doctype == "Gratuity": - outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount) - else: - outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) - else: - # Get the exchange rate based on the posting date of the ref doc. - exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) + if reference_doctype == "Fees": + total_amount = ref_doc.get("grand_total") + exchange_rate = 1 + outstanding_amount = ref_doc.get("outstanding_amount") + elif reference_doctype == "Dunning": + total_amount = ref_doc.get("dunning_amount") + exchange_rate = 1 + outstanding_amount = ref_doc.get("dunning_amount") + elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + total_amount = ref_doc.get("total_amount") + if ref_doc.multi_currency: + exchange_rate = get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) + else: + exchange_rate = 1 + outstanding_amount = get_outstanding_on_journal_entry(reference_name) + elif reference_doctype != "Journal Entry": + if ref_doc.doctype == "Sales Commission": + total_amount = ref_doc.total_commission_amount + exchange_rate = 1 + elif ref_doc.doctype == "Expense Claim": + total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) + elif ref_doc.doctype == "Employee Advance": + total_amount = ref_doc.advance_amount + exchange_rate = ref_doc.get("exchange_rate") + if party_account_currency != ref_doc.currency: + total_amount = flt(total_amount) * flt(exchange_rate) + elif ref_doc.doctype == "Gratuity": + total_amount = ref_doc.amount + if not total_amount: + if party_account_currency == company_currency: + total_amount = ref_doc.base_grand_total + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total + if not exchange_rate: + # Get the exchange rate from the original ref doc + # or get it based on the posting date of the ref doc. + exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) + if reference_doctype in ("Sales Invoice", "Purchase Invoice"): + outstanding_amount = ref_doc.get("outstanding_amount") + bill_no = ref_doc.get("bill_no") + elif reference_doctype == "Expense Claim": + outstanding_amount = ( + flt(ref_doc.get("total_sanctioned_amount")) + + flt(ref_doc.get("total_taxes_and_charges")) + - flt(ref_doc.get("total_amount_reimbursed")) + - flt(ref_doc.get("total_advance_amount")) + ) + elif reference_doctype == "Employee Advance": + outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount) + if party_account_currency != ref_doc.currency: + outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) + if party_account_currency == company_currency: + exchange_rate = 1 + elif reference_doctype == "Gratuity": + outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount) + elif reference_doctype == "Sales Commission": + outstanding_amount = 0 + else: + outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) + else: + # Get the exchange rate based on the posting date of the ref doc. + exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) - return frappe._dict( - { - "due_date": ref_doc.get("due_date"), - "total_amount": flt(total_amount), - "outstanding_amount": flt(outstanding_amount), - "exchange_rate": flt(exchange_rate), - "bill_no": bill_no, - } - ) + return frappe._dict( + { + "due_date": ref_doc.get("due_date"), + "total_amount": flt(total_amount), + "outstanding_amount": flt(outstanding_amount), + "exchange_rate": flt(exchange_rate), + "bill_no": bill_no, + } + ) def get_amounts_based_on_reference_doctype( - reference_doctype, ref_doc, party_account_currency, company_currency, reference_name + reference_doctype, ref_doc, party_account_currency, company_currency, reference_name ): - total_amount = outstanding_amount = exchange_rate = None - if reference_doctype == "Fees": - total_amount = ref_doc.get("grand_total") - exchange_rate = 1 - outstanding_amount = ref_doc.get("outstanding_amount") - elif reference_doctype == "Dunning": - total_amount = ref_doc.get("dunning_amount") - exchange_rate = 1 - outstanding_amount = ref_doc.get("dunning_amount") - elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: - total_amount = ref_doc.get("total_amount") - if ref_doc.multi_currency: - exchange_rate = get_exchange_rate( - party_account_currency, company_currency, ref_doc.posting_date - ) - else: - exchange_rate = 1 - outstanding_amount = get_outstanding_on_journal_entry(reference_name) + total_amount = outstanding_amount = exchange_rate = None + if reference_doctype == "Fees": + total_amount = ref_doc.get("grand_total") + exchange_rate = 1 + outstanding_amount = ref_doc.get("outstanding_amount") + elif reference_doctype == "Dunning": + total_amount = ref_doc.get("dunning_amount") + exchange_rate = 1 + outstanding_amount = ref_doc.get("dunning_amount") + elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: + total_amount = ref_doc.get("total_amount") + if ref_doc.multi_currency: + exchange_rate = get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) + else: + exchange_rate = 1 + outstanding_amount = get_outstanding_on_journal_entry(reference_name) - return total_amount, outstanding_amount, exchange_rate + return total_amount, outstanding_amount, exchange_rate def get_amounts_based_on_ref_doc( - reference_doctype, ref_doc, party_account_currency, company_currency + reference_doctype, ref_doc, party_account_currency, company_currency ): - total_amount = outstanding_amount = exchange_rate = None - if ref_doc.doctype == "Expense Claim": - total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) - elif ref_doc.doctype == "Employee Advance": - total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance( - party_account_currency, ref_doc - ) + total_amount = outstanding_amount = exchange_rate = None + if ref_doc.doctype == "Expense Claim": + total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) + elif ref_doc.doctype == "Employee Advance": + total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance( + party_account_currency, ref_doc + ) - if not total_amount: - total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( - party_account_currency, company_currency, ref_doc - ) + if not total_amount: + total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( + party_account_currency, company_currency, ref_doc + ) - if not exchange_rate: - # Get the exchange rate from the original ref doc - # or get it based on the posting date of the ref doc - exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate( - party_account_currency, company_currency, ref_doc.posting_date - ) + if not exchange_rate: + # Get the exchange rate from the original ref doc + # or get it based on the posting date of the ref doc + exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate( + party_account_currency, company_currency, ref_doc.posting_date + ) - outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts( - reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency - ) + outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts( + reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency + ) - return total_amount, outstanding_amount, exchange_rate, bill_no + return total_amount, outstanding_amount, exchange_rate, bill_no def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc): - total_amount = ref_doc.advance_amount - exchange_rate = ref_doc.get("exchange_rate") - if party_account_currency != ref_doc.currency: - total_amount = flt(total_amount) * flt(exchange_rate) + total_amount = ref_doc.advance_amount + exchange_rate = ref_doc.get("exchange_rate") + if party_account_currency != ref_doc.currency: + total_amount = flt(total_amount) * flt(exchange_rate) - return total_amount, exchange_rate + return total_amount, exchange_rate def get_total_amount_exchange_rate_base_on_currency( - party_account_currency, company_currency, ref_doc + party_account_currency, company_currency, ref_doc ): - exchange_rate = None - if party_account_currency == company_currency: - total_amount = ref_doc.base_grand_total - exchange_rate = 1 - else: - total_amount = ref_doc.grand_total + exchange_rate = None + if party_account_currency == company_currency: + total_amount = ref_doc.base_grand_total + exchange_rate = 1 + else: + total_amount = ref_doc.grand_total - return total_amount, exchange_rate + return total_amount, exchange_rate def get_bill_no_and_update_amounts( - reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency + reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency ): - outstanding_amount = bill_no = None - if reference_doctype in ("Sales Invoice", "Purchase Invoice"): - outstanding_amount = ref_doc.get("outstanding_amount") - bill_no = ref_doc.get("bill_no") - elif reference_doctype == "Expense Claim": - outstanding_amount = ( - flt(ref_doc.get("total_sanctioned_amount")) - + flt(ref_doc.get("total_taxes_and_charges")) - - flt(ref_doc.get("total_amount_reimbursed")) - - flt(ref_doc.get("total_advance_amount")) - ) - elif reference_doctype == "Employee Advance": - outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount) - if party_account_currency != ref_doc.currency: - outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) - if party_account_currency == company_currency: - exchange_rate = 1 - else: - outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) + outstanding_amount = bill_no = None + if reference_doctype in ("Sales Invoice", "Purchase Invoice"): + outstanding_amount = ref_doc.get("outstanding_amount") + bill_no = ref_doc.get("bill_no") + elif reference_doctype == "Expense Claim": + outstanding_amount = ( + flt(ref_doc.get("total_sanctioned_amount")) + + flt(ref_doc.get("total_taxes_and_charges")) + - flt(ref_doc.get("total_amount_reimbursed")) + - flt(ref_doc.get("total_advance_amount")) + ) + elif reference_doctype == "Employee Advance": + outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount) + if party_account_currency != ref_doc.currency: + outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) + if party_account_currency == company_currency: + exchange_rate = 1 + else: + outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) - return outstanding_amount, exchange_rate, bill_no + return outstanding_amount, exchange_rate, bill_no @frappe.whitelist() def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): - reference_doc = None - doc = frappe.get_doc(dt, dn) - if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: - frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) + reference_doc = None + doc = frappe.get_doc(dt, dn) + if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: + frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) - party_type = set_party_type(dt) - party_account = set_party_account(dt, dn, doc, party_type) - party_account_currency = set_party_account_currency(dt, party_account, doc) - payment_type = set_payment_type(dt, doc) - grand_total, outstanding_amount = set_grand_total_and_outstanding_amount( - party_amount, dt, party_account_currency, doc - ) + party_type = set_party_type(dt) + party_account = set_party_account(dt, dn, doc, party_type) + party_account_currency = set_party_account_currency(dt, party_account, doc) + payment_type = set_payment_type(dt, doc) + grand_total, outstanding_amount = set_grand_total_and_outstanding_amount( + party_amount, dt, party_account_currency, doc + ) - # bank or cash - bank = get_bank_cash_account(doc, bank_account) + # bank or cash + bank = get_bank_cash_account(doc, bank_account) - paid_amount, received_amount = set_paid_amount_and_received_amount( - dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc - ) + paid_amount, received_amount = set_paid_amount_and_received_amount( + dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc + ) - paid_amount, received_amount, discount_amount = apply_early_payment_discount( - paid_amount, received_amount, doc - ) + paid_amount, received_amount, discount_amount = apply_early_payment_discount( + paid_amount, received_amount, doc + ) - pe = frappe.new_doc("Payment Entry") - pe.payment_type = payment_type - pe.company = doc.company - pe.cost_center = doc.get("cost_center") - pe.posting_date = nowdate() - pe.mode_of_payment = doc.get("mode_of_payment") - pe.party_type = party_type - pe.party = doc.get(scrub(party_type)) - pe.contact_person = doc.get("contact_person") - pe.contact_email = doc.get("contact_email") - pe.ensure_supplier_is_not_blocked() + pe = frappe.new_doc("Payment Entry") + pe.payment_type = payment_type + pe.company = doc.company + pe.cost_center = doc.get("cost_center") + pe.posting_date = nowdate() + pe.mode_of_payment = doc.get("mode_of_payment") + pe.party_type = party_type + pe.party = doc.get(scrub(party_type)) + pe.contact_person = doc.get("contact_person") + pe.contact_email = doc.get("contact_email") + pe.ensure_supplier_is_not_blocked() - pe.paid_from = party_account if payment_type == "Receive" else bank.account - pe.paid_to = party_account if payment_type == "Pay" else bank.account - pe.paid_from_account_currency = ( - party_account_currency if payment_type == "Receive" else bank.account_currency - ) - pe.paid_to_account_currency = ( - party_account_currency if payment_type == "Pay" else bank.account_currency - ) - pe.paid_amount = paid_amount - pe.received_amount = received_amount - pe.letter_head = doc.get("letter_head") + pe.paid_from = party_account if payment_type == "Receive" else bank.account + pe.paid_to = party_account if payment_type == "Pay" else bank.account + pe.paid_from_account_currency = ( + party_account_currency if payment_type == "Receive" else bank.account_currency + ) + pe.paid_to_account_currency = ( + party_account_currency if payment_type == "Pay" else bank.account_currency + ) + pe.paid_amount = paid_amount + pe.received_amount = received_amount + pe.letter_head = doc.get("letter_head") - if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]: - pe.project = doc.get("project") or reduce( - lambda prev, cur: prev or cur, [x.get("project") for x in doc.get("items")], None - ) # get first non-empty project from items + if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]: + pe.project = doc.get("project") or reduce( + lambda prev, cur: prev or cur, [x.get("project") for x in doc.get("items")], None + ) # get first non-empty project from items - if pe.party_type in ["Customer", "Supplier"]: - bank_account = get_party_bank_account(pe.party_type, pe.party) - pe.set("bank_account", bank_account) - pe.set_bank_account_data() + if pe.party_type in ["Customer", "Supplier"]: + bank_account = get_party_bank_account(pe.party_type, pe.party) + pe.set("bank_account", bank_account) + pe.set_bank_account_data() - # only Purchase Invoice can be blocked individually - if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): - frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date)) - else: - if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value( - "Payment Terms Template", - {"name": doc.payment_terms_template}, - "allocate_payment_based_on_payment_terms", - ): + # only Purchase Invoice can be blocked individually + if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): + frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date)) + else: + if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value( + "Payment Terms Template", + {"name": doc.payment_terms_template}, + "allocate_payment_based_on_payment_terms", + ): - for reference in get_reference_as_per_payment_terms( - doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount - ): - pe.append("references", reference) - else: - if dt == "Dunning": - pe.append( - "references", - { - "reference_doctype": "Sales Invoice", - "reference_name": doc.get("sales_invoice"), - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": doc.get("outstanding_amount"), - "outstanding_amount": doc.get("outstanding_amount"), - "allocated_amount": doc.get("outstanding_amount"), - }, - ) - pe.append( - "references", - { - "reference_doctype": dt, - "reference_name": dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": doc.get("dunning_amount"), - "outstanding_amount": doc.get("dunning_amount"), - "allocated_amount": doc.get("dunning_amount"), - }, - ) - else: - pe.append( - "references", - { - "reference_doctype": dt, - "reference_name": dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": grand_total, - "outstanding_amount": outstanding_amount, - "allocated_amount": outstanding_amount, - }, - ) + for reference in get_reference_as_per_payment_terms( + doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount + ): + pe.append("references", reference) + else: + if dt == "Dunning": + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": doc.get("sales_invoice"), + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": doc.get("outstanding_amount"), + "outstanding_amount": doc.get("outstanding_amount"), + "allocated_amount": doc.get("outstanding_amount"), + }, + ) + pe.append( + "references", + { + "reference_doctype": dt, + "reference_name": dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": doc.get("dunning_amount"), + "outstanding_amount": doc.get("dunning_amount"), + "allocated_amount": doc.get("dunning_amount"), + }, + ) + else: + pe.append( + "references", + { + "reference_doctype": dt, + "reference_name": dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": grand_total, + "outstanding_amount": outstanding_amount, + "allocated_amount": outstanding_amount, + }, + ) - pe.setup_party_account_field() - pe.set_missing_values() + pe.setup_party_account_field() + pe.set_missing_values() - if party_account and bank: - if dt == "Employee Advance": - reference_doc = doc - pe.set_exchange_rate(ref_doc=reference_doc) - pe.set_amounts() - if discount_amount: - pe.set_gain_or_loss( - account_details={ - "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), - "cost_center": pe.cost_center - or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": discount_amount * (-1 if payment_type == "Pay" else 1), - } - ) - pe.set_difference_amount() + if party_account and bank: + if dt == "Employee Advance": + reference_doc = doc + pe.set_exchange_rate(ref_doc=reference_doc) + pe.set_amounts() + if discount_amount: + pe.set_gain_or_loss( + account_details={ + "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), + "cost_center": pe.cost_center + or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": discount_amount * (-1 if payment_type == "Pay" else 1), + } + ) + pe.set_difference_amount() - return pe + return pe def get_bank_cash_account(doc, bank_account): - bank = get_default_bank_cash_account( - doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account - ) + bank = get_default_bank_cash_account( + doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account + ) - if not bank: - bank = get_default_bank_cash_account( - doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), account=bank_account - ) + if not bank: + bank = get_default_bank_cash_account( + doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), account=bank_account + ) - return bank + return bank def set_party_type(dt): - if dt in ("Sales Invoice", "Sales Order", "Dunning"): - party_type = "Customer" - elif dt in ("Purchase Invoice", "Purchase Order"): - party_type = "Supplier" - elif dt in ("Expense Claim", "Employee Advance", "Gratuity"): - party_type = "Employee" - return party_type + if dt in ("Sales Invoice", "Sales Order", "Dunning"): + party_type = "Customer" + elif dt in ("Purchase Invoice", "Purchase Order"): + party_type = "Supplier" + elif dt in ("Expense Claim", "Employee Advance", "Gratuity"): + party_type = "Employee" + return party_type def set_party_account(dt, dn, doc, party_type): - if dt == "Sales Invoice": - party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to - elif dt == "Purchase Invoice": - party_account = doc.credit_to - elif dt == "Fees": - party_account = doc.receivable_account - elif dt == "Employee Advance": - party_account = doc.advance_account - elif dt == "Expense Claim": - party_account = doc.payable_account - elif dt == "Gratuity": - party_account = doc.payable_account - else: - party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) - return party_account + if dt == "Sales Invoice": + party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to + elif dt == "Purchase Invoice": + party_account = doc.credit_to + elif dt == "Fees": + party_account = doc.receivable_account + elif dt == "Employee Advance": + party_account = doc.advance_account + elif dt == "Expense Claim": + party_account = doc.payable_account + elif dt == "Gratuity": + party_account = doc.payable_account + else: + party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) + return party_account def set_party_account_currency(dt, party_account, doc): - if dt not in ("Sales Invoice", "Purchase Invoice"): - party_account_currency = get_account_currency(party_account) - else: - party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) - return party_account_currency + if dt not in ("Sales Invoice", "Purchase Invoice"): + party_account_currency = get_account_currency(party_account) + else: + party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) + return party_account_currency def set_payment_type(dt, doc): - if ( - dt == "Sales Order" - or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0) - ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): - payment_type = "Receive" - else: - payment_type = "Pay" - return payment_type + if ( + dt == "Sales Order" + or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0) + ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): + payment_type = "Receive" + else: + payment_type = "Pay" + return payment_type def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc): - grand_total = outstanding_amount = 0 - if party_amount: - grand_total = outstanding_amount = party_amount - elif dt in ("Sales Invoice", "Purchase Invoice"): - if party_account_currency == doc.company_currency: - grand_total = doc.base_rounded_total or doc.base_grand_total - else: - grand_total = doc.rounded_total or doc.grand_total - outstanding_amount = doc.outstanding_amount - elif dt in ("Expense Claim"): - grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges - outstanding_amount = doc.grand_total - doc.total_amount_reimbursed - elif dt == "Employee Advance": - grand_total = flt(doc.advance_amount) - outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) - if party_account_currency != doc.currency: - grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate) - outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate) - elif dt == "Fees": - grand_total = doc.grand_total - outstanding_amount = doc.outstanding_amount - elif dt == "Dunning": - grand_total = doc.grand_total - outstanding_amount = doc.grand_total - elif dt == "Gratuity": - grand_total = doc.amount - outstanding_amount = flt(doc.amount) - flt(doc.paid_amount) - else: - if party_account_currency == doc.company_currency: - grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) - else: - grand_total = flt(doc.get("rounded_total") or doc.grand_total) - outstanding_amount = grand_total - flt(doc.advance_paid) - return grand_total, outstanding_amount + grand_total = outstanding_amount = 0 + if party_amount: + grand_total = outstanding_amount = party_amount + elif dt in ("Sales Invoice", "Purchase Invoice"): + if party_account_currency == doc.company_currency: + grand_total = doc.base_rounded_total or doc.base_grand_total + else: + grand_total = doc.rounded_total or doc.grand_total + outstanding_amount = doc.outstanding_amount + elif dt in ("Expense Claim"): + grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges + outstanding_amount = doc.grand_total - doc.total_amount_reimbursed + elif dt == "Employee Advance": + grand_total = flt(doc.advance_amount) + outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) + if party_account_currency != doc.currency: + grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate) + outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate) + elif dt == "Fees": + grand_total = doc.grand_total + outstanding_amount = doc.outstanding_amount + elif dt == "Dunning": + grand_total = doc.grand_total + outstanding_amount = doc.grand_total + elif dt == "Gratuity": + grand_total = doc.amount + outstanding_amount = flt(doc.amount) - flt(doc.paid_amount) + else: + if party_account_currency == doc.company_currency: + grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) + else: + grand_total = flt(doc.get("rounded_total") or doc.grand_total) + outstanding_amount = grand_total - flt(doc.advance_paid) + return grand_total, outstanding_amount def set_paid_amount_and_received_amount( - dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc + dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ): - paid_amount = received_amount = 0 - if party_account_currency == bank.account_currency: - paid_amount = received_amount = abs(outstanding_amount) - elif payment_type == "Receive": - paid_amount = abs(outstanding_amount) - if bank_amount: - received_amount = bank_amount - else: - received_amount = paid_amount * doc.get("conversion_rate", 1) - if dt == "Employee Advance": - received_amount = paid_amount * doc.get("exchange_rate", 1) - else: - received_amount = abs(outstanding_amount) - if bank_amount: - paid_amount = bank_amount - else: - # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get("conversion_rate", 1) - if dt == "Employee Advance": - paid_amount = received_amount * doc.get("exchange_rate", 1) + paid_amount = received_amount = 0 + if party_account_currency == bank.account_currency: + paid_amount = received_amount = abs(outstanding_amount) + elif payment_type == "Receive": + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount + else: + received_amount = paid_amount * doc.get("conversion_rate", 1) + if dt == "Employee Advance": + received_amount = paid_amount * doc.get("exchange_rate", 1) + else: + received_amount = abs(outstanding_amount) + if bank_amount: + paid_amount = bank_amount + else: + # if party account currency and bank currency is different then populate paid amount as well + paid_amount = received_amount * doc.get("conversion_rate", 1) + if dt == "Employee Advance": + paid_amount = received_amount * doc.get("exchange_rate", 1) - return paid_amount, received_amount + return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): - total_discount = 0 - eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] - has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule + total_discount = 0 + eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] + has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule - if doc.doctype in eligible_for_payments and has_payment_schedule: - for term in doc.payment_schedule: - if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: - if term.discount_type == "Percentage": - discount_amount = flt(doc.get("grand_total")) * (term.discount / 100) - else: - discount_amount = term.discount + if doc.doctype in eligible_for_payments and has_payment_schedule: + for term in doc.payment_schedule: + if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: + if term.discount_type == "Percentage": + discount_amount = flt(doc.get("grand_total")) * (term.discount / 100) + else: + discount_amount = term.discount - discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1) + discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1) - if doc.doctype == "Sales Invoice": - paid_amount -= discount_amount - received_amount -= discount_amount_in_foreign_currency - else: - received_amount -= discount_amount - paid_amount -= discount_amount_in_foreign_currency + if doc.doctype == "Sales Invoice": + paid_amount -= discount_amount + received_amount -= discount_amount_in_foreign_currency + else: + received_amount -= discount_amount + paid_amount -= discount_amount_in_foreign_currency - total_discount += discount_amount + total_discount += discount_amount - if total_discount: - money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) - frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) + if total_discount: + money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) + frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) - return paid_amount, received_amount, total_discount + return paid_amount, received_amount, total_discount def get_reference_as_per_payment_terms( - payment_schedule, dt, dn, doc, grand_total, outstanding_amount + payment_schedule, dt, dn, doc, grand_total, outstanding_amount ): - references = [] - for payment_term in payment_schedule: - payment_term_outstanding = flt( - payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount") - ) + references = [] + for payment_term in payment_schedule: + payment_term_outstanding = flt( + payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount") + ) - if payment_term_outstanding: - references.append( - { - "reference_doctype": dt, - "reference_name": dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - "total_amount": grand_total, - "outstanding_amount": outstanding_amount, - "payment_term": payment_term.payment_term, - "allocated_amount": payment_term_outstanding, - } - ) + if payment_term_outstanding: + references.append( + { + "reference_doctype": dt, + "reference_name": dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + "total_amount": grand_total, + "outstanding_amount": outstanding_amount, + "payment_term": payment_term.payment_term, + "allocated_amount": payment_term_outstanding, + } + ) - return references + return references def get_paid_amount(dt, dn, party_type, party, account, due_date): - if party_type == "Customer": - dr_or_cr = "credit_in_account_currency - debit_in_account_currency" - else: - dr_or_cr = "debit_in_account_currency - credit_in_account_currency" + if party_type == "Customer": + dr_or_cr = "credit_in_account_currency - debit_in_account_currency" + else: + dr_or_cr = "debit_in_account_currency - credit_in_account_currency" - paid_amount = frappe.db.sql( - """ - select ifnull(sum({dr_or_cr}), 0) as paid_amount - from `tabGL Entry` - where against_voucher_type = %s - and against_voucher = %s - and party_type = %s - and party = %s - and account = %s - and due_date = %s - and {dr_or_cr} > 0 - """.format( - dr_or_cr=dr_or_cr - ), - (dt, dn, party_type, party, account, due_date), - ) + paid_amount = frappe.db.sql( + """ + select ifnull(sum({dr_or_cr}), 0) as paid_amount + from `tabGL Entry` + where against_voucher_type = %s + and against_voucher = %s + and party_type = %s + and party = %s + and account = %s + and due_date = %s + and {dr_or_cr} > 0 + """.format( + dr_or_cr=dr_or_cr + ), + (dt, dn, party_type, party, account, due_date), + ) - return paid_amount[0][0] if paid_amount else 0 + return paid_amount[0][0] if paid_amount else 0 @frappe.whitelist() def get_party_and_account_balance( - company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None + company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None ): - return frappe._dict( - { - "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center), - "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center), - "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center), - } - ) + return frappe._dict( + { + "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center), + "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center), + "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center), + } + ) @frappe.whitelist() def make_payment_order(source_name, target_doc=None): - from frappe.model.mapper import get_mapped_doc + from frappe.model.mapper import get_mapped_doc - def set_missing_values(source, target): - target.payment_order_type = "Payment Entry" - target.append( - "references", - dict( - reference_doctype="Payment Entry", - reference_name=source.name, - bank_account=source.party_bank_account, - amount=source.paid_amount, - account=source.paid_to, - supplier=source.party, - mode_of_payment=source.mode_of_payment, - ), - ) + def set_missing_values(source, target): + target.payment_order_type = "Payment Entry" + target.append( + "references", + dict( + reference_doctype="Payment Entry", + reference_name=source.name, + bank_account=source.party_bank_account, + amount=source.paid_amount, + account=source.paid_to, + supplier=source.party, + mode_of_payment=source.mode_of_payment, + ), + ) - doclist = get_mapped_doc( - "Payment Entry", - source_name, - { - "Payment Entry": { - "doctype": "Payment Order", - "validation": {"docstatus": ["=", 1]}, - } - }, - target_doc, - set_missing_values, - ) + doclist = get_mapped_doc( + "Payment Entry", + source_name, + { + "Payment Entry": { + "doctype": "Payment Order", + "validation": {"docstatus": ["=", 1]}, + } + }, + target_doc, + set_missing_values, + ) - return doclist + return doclist diff --git a/erpnext/payroll/doctype/contributions/__init__.py b/erpnext/payroll/doctype/contributions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/contributions/contributions.json b/erpnext/payroll/doctype/contributions/contributions.json new file mode 100644 index 00000000000..05fe414017a --- /dev/null +++ b/erpnext/payroll/doctype/contributions/contributions.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-09-07 12:49:18.526652", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "order_or_invoice", + "customer", + "customer_name", + "posting_date", + "column_break_5", + "contribution_percent", + "contribution_amount", + "commission_rate", + "commission_amount" + ], + "fields": [ + { + "fieldname": "order_or_invoice", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Order / Invoice", + "options": "document_type", + "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer", + "read_only": 1 + }, + { + "fetch_from": "customer.customer_name", + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "read_only": 1 + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "read_only": 1 + }, + { + "fieldname": "commission_rate", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Commission Rate", + "read_only": 1 + }, + { + "fieldname": "commission_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Commission Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "hidden": 1, + "label": "Document Type", + "options": "DocType" + }, + { + "fieldname": "contribution_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Contribution Amount", + "read_only": 1 + }, + { + "fieldname": "contribution_percent", + "fieldtype": "Data", + "label": "Contribution %", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-13 19:11:43.548342", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Contributions", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/contributions/contributions.py b/erpnext/payroll/doctype/contributions/contributions.py new file mode 100644 index 00000000000..ec69202d612 --- /dev/null +++ b/erpnext/payroll/doctype/contributions/contributions.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class Contributions(Document): + pass diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json index 54377e94b30..fb307f30fc6 100644 --- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json @@ -9,6 +9,7 @@ "payroll_based_on", "consider_unmarked_attendance_as", "max_working_hours_against_timesheet", + "salary_component_for_sales_commission", "include_holidays_in_total_working_days", "disable_rounded_total", "column_break_11", @@ -91,13 +92,19 @@ "fieldname": "show_leave_balances_in_salary_slip", "fieldtype": "Check", "label": "Show Leave Balances in Salary Slip" + }, + { + "fieldname": "salary_component_for_sales_commission", + "fieldtype": "Link", + "label": "Salary Component for Sales Commission", + "options": "Salary Component" } ], "icon": "fa fa-cog", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-03-03 17:49:59.579723", + "modified": "2021-09-07 12:21:16.640474", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Settings", diff --git a/erpnext/payroll/doctype/process_sales_commission/__init__.py b/erpnext/payroll/doctype/process_sales_commission/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.js b/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.js new file mode 100644 index 00000000000..6b7f239f376 --- /dev/null +++ b/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.js @@ -0,0 +1,17 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Sales Commission', { + setup: function(frm){ + frm.set_query("department", function() { + if (!frm.doc.company) { + frappe.throw(__("Please select company first")) + } + return { + filters: { + company: frm.doc.company + } + } + }); + }, +}); diff --git a/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.json b/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.json new file mode 100644 index 00000000000..37ee5085338 --- /dev/null +++ b/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.json @@ -0,0 +1,145 @@ +{ + "actions": [], + "autoname": "format:PRO-SAL-COM-{#####}", + "creation": "2021-09-08 13:28:11.658071", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "department", + "column_break_3", + "designation", + "branch", + "section_break_6", + "from_date", + "to_date", + "column_break_9", + "commission_based_on", + "pay_via_salary", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation" + }, + { + "fieldname": "branch", + "fieldtype": "Link", + "label": "Branch", + "options": "Branch" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "reqd": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "commission_based_on", + "fieldtype": "Select", + "label": "Commission Based on", + "options": "Sales Order\nSales Invoice" + }, + { + "default": "0", + "fieldname": "pay_via_salary", + "fieldtype": "Check", + "label": "Pay Via Salary" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Sales Commission", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-09-20 15:45:39.240487", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Process Sales Commission", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.py b/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.py new file mode 100644 index 00000000000..201ea97985c --- /dev/null +++ b/erpnext/payroll/doctype/process_sales_commission/process_sales_commission.py @@ -0,0 +1,98 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form + + +class ProcessSalesCommission(Document): + def validate(self): + self.validate_from_to_dates() + self.validate_salary_component() + + def validate_from_to_dates(self): + return super().validate_from_to_dates("from_date", "to_date") + + def validate_salary_component(self): + if self.pay_via_salary: + if not frappe.db.get_single_value("Payroll Settings", "salary_component_for_sales_commission"): + frappe.throw(_("Please set {0} in {1}").format(frappe.bold("Salary Component for Sales Commission"), get_link_to_form("Payroll Settings", "Payroll Settings"))) + + def on_submit(self): + self.process_sales_commission() + + def process_sales_commission(self): + filter_date = "transaction_date" if self.commission_based_on=="Sales Order" else "posting_date" + records = [entry.name for entry in frappe.db.get_all(self.commission_based_on, filters={ "company": self.company, filter_date: ('between', [self.from_date, self.to_date])})] + sales_persons_details = frappe.get_all("Sales Team", filters={"parent": ['in', records]}, fields=["sales_person", "commission_rate", "incentives", "allocated_percentage", "allocated_amount", "parent"]) + if len(sales_persons_details): + sales_persons = set(e['sales_person'] for e in sales_persons_details) + sales_persons_list = self.get_sales_persons_list(sales_persons) + sales_persons_details_map = self.map_sales_persons_details(sales_persons_list, sales_persons_details) + self.make_sales_commission_document(sales_persons_details_map, filter_date) + + def get_sales_persons_list(self, sales_persons): + sales_persons_list = sales_persons + if self.department or self.designation or self.branch: + for person in sales_persons: + emp = frappe.db.get_value("Sales Person", filters={"name":person}, fieldname="employee", as_dict=True)['employee'] + if emp: + employee_details = frappe.db.get_value("Employee", filters={"name":emp}, fieldname=["company", "department", "designation", "branch"], as_dict=True) + if self.company != employee_details["company"]: + sales_persons_list.remove(person) + continue + if self.department and self.department != employee_details["department"]: + sales_persons_list.remove(person) + continue + if self.designation and self.designation != employee_details["designation"]: + sales_persons_list.remove(person) + continue + if self.branch and self.branch != employee_details["branch"]: + sales_persons_list.remove(person) + continue + + return sales_persons_list + + def map_sales_persons_details(self, sales_persons, sales_persons_details): + sales_persons_details_map = {} + for person in sales_persons: + sales_persons_details_map[person] = [] + for details in sales_persons_details: + if details['sales_person'] == person: + sales_persons_details_map[person].append(details) + + return sales_persons_details_map + + def make_sales_commission_document(self, sales_persons_details_map, filter_date): + for record in sales_persons_details_map: + doc = doc = frappe.new_doc("Sales Commission") + doc.sales_person = record + doc.from_date = self.from_date + doc.to_date = self.to_date + doc.pay_via_salary = self.pay_via_salary + doc.process_sales_commission_reference = self.name + doc.set("contributions", []) + self.add_contributions(doc, sales_persons_details_map[record], filter_date) + doc.insert() + if not frappe.db.get_single_value("Selling Settings", "approval_required_for_sales_commission_payout"): + doc.reload() + if self.pay_via_salary and doc.employee: + if frappe.db.exists('Salary Structure Assignment', {'employee': doc.employee}): + doc.submit() + + def add_contributions(self, doc, records, filter_date): + for items in records: + sales_record_details = frappe.db.get_value(self.commission_based_on, filters={"name": items["parent"]}, fieldname=["customer", filter_date], as_dict=True) + contribution = { + "document_type": self.commission_based_on, + "order_or_invoice": items["parent"], + "customer": sales_record_details["customer"], + "posting_date": sales_record_details[filter_date], + "contribution_percent": items["allocated_percentage"], + "contribution_amount": items["allocated_amount"], + "commission_rate": items["commission_rate"], + "commission_amount": items["incentives"], + } + doc.append("contributions", contribution) \ No newline at end of file diff --git a/erpnext/payroll/doctype/process_sales_commission/test_process_sales_commission.py b/erpnext/payroll/doctype/process_sales_commission/test_process_sales_commission.py new file mode 100644 index 00000000000..8ef2dc9aa2d --- /dev/null +++ b/erpnext/payroll/doctype/process_sales_commission/test_process_sales_commission.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestProcessSalesCommission(unittest.TestCase): + pass diff --git a/erpnext/payroll/doctype/sales_commission/__init__.py b/erpnext/payroll/doctype/sales_commission/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/sales_commission/sales_commission.js b/erpnext/payroll/doctype/sales_commission/sales_commission.js new file mode 100644 index 00000000000..3cfa8d2a951 --- /dev/null +++ b/erpnext/payroll/doctype/sales_commission/sales_commission.js @@ -0,0 +1,118 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Sales Commission', { + setup: function(frm){ + frm.set_query("commission_based_on", function() { + return { + filters: [ + ['name', 'in', ["Sales Order", "Sales Invoice"]] + ] + } + }); + }, + refresh: function(frm) { + if (frm.doc.docstatus == 1) { + if (frm.custom_buttons) frm.clear_custom_buttons(); + frm.events.add_context_buttons(frm); + } + }, + + add_context_buttons: function (frm) { + if (!frm.doc.reference_name) { + if (frm.doc.pay_via_salary) { + frm.add_custom_button(__("Create Additional Salary"), function () { + create_additional_salary(frm); + }).addClass("btn-primary"); + } else { + frm.add_custom_button(__("Create Payment Entry"), function () { + create_payment_entry(frm); + }).addClass("btn-primary"); + } + } + }, + +}); + +const create_payment_entry = function (frm) { + var d = new frappe.ui.Dialog({ + title: __("Select Mode of Payment"), + fields: [ + { + 'fieldname': 'mode_of_payment', + 'fieldtype': 'Link', + 'label': __('Mode of Payment'), + 'options': 'Mode of Payment', + "get_query": function () { + return { + filters: { + type: ["in", ["Bank", "Cash"]] + } + }; + }, + 'reqd': 1 + } + ], + }); + d.set_primary_action(__('Create'), function() { + d.hide(); + var arg = d.get_values(); + frappe.confirm(__("Creating Payment Entry. Do you want to proceed?"), + function () { + frappe.call({ + method: 'payout_entry', + args: { + "mode_of_payment": arg.mode_of_payment + }, + callback: function () { + frappe.set_route( + 'Form', "Payment Entry", { + "Payment Entry Reference.reference_name": frm.doc.name + } + ); + }, + doc: frm.doc, + freeze: true, + freeze_message: __('Creating Payment Entry') + }); + }, + function () { + if (frappe.dom.freeze_count) { + frappe.dom.unfreeze(); + frm.events.refresh(frm); + } + } + ); + }); + d.show(); +}; + +const create_additional_salary = function (frm) { + if (!frm.doc.employee) { + frappe.throw(__("No employee is linked to Sales Person {0}. Please select an employee for {1} to process this Commission.").format(frappe.bold(frm.doc.sales_person), get_link_to_form("Sales Person", frm.doc.sales_person))) + } + frappe.confirm(__("Creating Additional Salary. Do you want to proceed?"), + function () { + frappe.call({ + method: 'payout_entry', + args: {}, + callback: function () { + frappe.set_route( + "Form", "Additional Salary", { + "Additional Salary.ref_docname": frm.doc.name + } + ); + }, + doc: frm.doc, + freeze: true, + freeze_message: __('Creating Additional Salary') + }); + }, + function () { + if (frappe.dom.freeze_count) { + frappe.dom.unfreeze(); + frm.events.refresh(frm); + } + } + ); +}; \ No newline at end of file diff --git a/erpnext/payroll/doctype/sales_commission/sales_commission.json b/erpnext/payroll/doctype/sales_commission/sales_commission.json new file mode 100644 index 00000000000..f2437320ff1 --- /dev/null +++ b/erpnext/payroll/doctype/sales_commission/sales_commission.json @@ -0,0 +1,263 @@ +{ + "actions": [], + "autoname": "format:SAL-COM-{#####}", + "creation": "2021-09-07 12:43:03.200379", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_person", + "employee", + "employee_name", + "designation", + "department", + "branch", + "column_break_6", + "status", + "company", + "pay_via_salary", + "section_break_10", + "from_date", + "to_date", + "column_break_13", + "commission_based_on", + "process_sales_commission_reference", + "section_break_15", + "contributions", + "section_break_17", + "total_contribution", + "total_commission_amount", + "remarks", + "column_break_21", + "commission_rate", + "calculate_commission_manually", + "amended_from", + "reference_doctype", + "reference_name" + ], + "fields": [ + { + "fetch_from": "sales_person.employee", + "fieldname": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee", + "read_only": 1 + }, + { + "depends_on": "employee", + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fetch_from": "employee.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fetch_from": "employee.branch", + "fieldname": "branch", + "fieldtype": "Link", + "label": "Branch", + "options": "Branch", + "read_only": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Draft\nUnpaid\nPaid", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "pay_via_salary", + "fieldtype": "Check", + "label": "Pay Via Salary" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, + { + "fieldname": "commission_based_on", + "fieldtype": "Select", + "label": "Commission Based on", + "options": "Sales Order\nSales Invoice" + }, + { + "fieldname": "total_contribution", + "fieldtype": "Currency", + "label": "Total Contribution", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "calculate_commission_manually", + "fieldtype": "Check", + "label": "Calculate Commission Manually" + }, + { + "depends_on": "calculate_commission_manually", + "fieldname": "commission_rate", + "fieldtype": "Float", + "label": "Commission Rate" + }, + { + "fieldname": "total_commission_amount", + "fieldtype": "Currency", + "label": "Total Commission Amount", + "read_only": 1 + }, + { + "fieldname": "remarks", + "fieldtype": "Data", + "hidden": 1, + "label": "Remarks" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Sales Commission", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "contributions", + "fieldtype": "Table", + "label": "Contributions", + "options": "Contributions" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_15", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_17", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "fieldname": "process_sales_commission_reference", + "fieldtype": "Link", + "label": "Process Sales Commission Reference", + "options": "Process Sales Commission", + "read_only": 1 + }, + { + "fieldname": "sales_person", + "fieldtype": "Link", + "label": "Sales Person", + "options": "Sales Person" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 1, + "label": "Reference Doctype", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 1, + "label": "Reference Name", + "options": "reference_doctype" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-09-20 15:46:26.805073", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Sales Commission", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/sales_commission/sales_commission.py b/erpnext/payroll/doctype/sales_commission/sales_commission.py new file mode 100644 index 00000000000..4187f816a4a --- /dev/null +++ b/erpnext/payroll/doctype/sales_commission/sales_commission.py @@ -0,0 +1,103 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form + + +class SalesCommission(Document): + def validate(self): + self.validate_from_to_dates() + self.validate_salary_component() + self.calculate_total_contribution_and_total_commission_amount() + + def validate_from_to_dates(self): + return super().validate_from_to_dates("from_date", "to_date") + + def validate_salary_component(self): + if self.pay_via_salary: + if not frappe.db.get_single_value("Payroll Settings", "salary_component_for_sales_commission"): + frappe.throw(_("Please set {0} in {1}").format(frappe.bold("Salary Component for Sales Commission"), get_link_to_form("Payroll Settings", "Payroll Settings"))) + + def calculate_total_contribution_and_total_commission_amount(self): + total_contribution, total_commission_amount = 0,0 + for entry in self.contributions: + total_contribution += entry.contribution_amount + total_commission_amount += entry.commission_amount + + if self.calculate_commission_manually: + rate = self.commission_rate + total_commission_amount = total_contribution * (rate / 100) + + self.total_contribution = total_contribution + self.total_commission_amount = total_commission_amount + + def on_submit(self): + if not self.employee: + frappe.throw(_("No employee is linked to Sales Person: {0}. Please select an employee for {1} to submit this document.").format(frappe.bold(self.sales_person), get_link_to_form("Sales Person", self.sales_person))) + if self.pay_via_salary: + self.make_additional_salary() + else: + self.make_payment_entry() + + @frappe.whitelist() + def payout_entry(self, mode_of_payment=None): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account + if mode_of_payment: + paid_from = get_bank_cash_account(mode_of_payment, self.company).get("account") + + paid_to = frappe.db.get_value("Company", filters={"name":self.company}, fieldname=['default_payable_account'], as_dict=True)['default_payable_account'] + if not paid_to: + frappe.throw(_("Please set Default Payable Account in {}").format(get_link_to_form("Company", self.company))) + if self.pay_via_salary: + self.make_additional_salary() + else: + self.make_payment_entry(mode_of_payment, paid_from, paid_to) + + + def make_additional_salary(self): + doc = frappe.new_doc("Additional Salary") + doc.employee = self.employee + doc.company = self.company + doc.salary_component = frappe.db.get_single_value("Payroll Settings", "salary_component_for_sales_commission") + doc.payroll_date = self.to_date + doc.amount = self.total_commission_amount + doc.ref_doctype = self.doctype + doc.ref_docname = self.name + + doc.submit() + + self.db_set("reference_doctype", "Additional Salary") + self.db_set("reference_name", doc.name) + + def make_payment_entry(self, mode_of_payment, paid_from, paid_to): + doc = frappe.new_doc("Payment Entry") + doc.company = self.company + doc.payment_type = "Pay" + doc.mode_of_payment = mode_of_payment + doc.party_type = "Employee" + doc.party = self.employee + doc.paid_from = paid_from + doc.paid_to = paid_to + doc.paid_amount = self.total_commission_amount + doc.received_amount = self.total_commission_amount + doc.source_exchange_rate = 1 + doc.target_exchange_rate = 1 + doc.set("references", []) + self.add_references(doc) + doc.submit() + + self.db_set("reference_doctype", "Payment Entry") + self.db_set("reference_name", doc.name) + + def add_references(self, doc): + reference = {} + reference['reference_doctype'] = "Sales Commission" + reference['reference_name'] = self.name + reference['due_date'] = self.to_date + reference['total_amount'] = self.total_commission_amount + reference['outstanding_amount'] = self.total_commission_amount + reference['allocated_amount'] = self.total_commission_amount + doc.append("references", reference) \ No newline at end of file diff --git a/erpnext/payroll/doctype/sales_commission/test_sales_commission.py b/erpnext/payroll/doctype/sales_commission/test_sales_commission.py new file mode 100644 index 00000000000..8cc43529532 --- /dev/null +++ b/erpnext/payroll/doctype/sales_commission/test_sales_commission.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestSalesCommission(unittest.TestCase): + pass diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 005e24cfbe3..c22582da745 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -28,7 +28,7 @@ "allow_multiple_items", "allow_against_multiple_purchase_orders", "hide_tax_id", - "enable_discount_accounting" + "approval_required_for_sales_commission_payout" ], "fields": [ { @@ -168,10 +168,9 @@ }, { "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", + "fieldname": "approval_required_for_sales_commission_payout", "fieldtype": "Check", - "label": "Enable Discount Accounting for Selling" + "label": "Approval Required for Sales Commission Payout" } ], "icon": "fa fa-cog", @@ -179,7 +178,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-14 16:01:29.405642", + "modified": "2022-05-14 16:01:29.405642", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -199,4 +198,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +}