Merge branch 'develop' into hr-separation
This commit is contained in:
commit
6a65c8aff4
1
.github/workflows/patch.yml
vendored
1
.github/workflows/patch.yml
vendored
@ -115,4 +115,5 @@ jobs:
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
bench setup requirements --python
|
||||
bench --site test_site migrate
|
||||
|
12
.mergify.yml
12
.mergify.yml
@ -9,6 +9,8 @@ pull_request_rules:
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=mergify[bot]
|
||||
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
@ -19,6 +21,16 @@ pull_request_rules:
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: Auto-close PRs on pre-release branch
|
||||
conditions:
|
||||
- base=version-13-pre-release
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
|
||||
|
||||
|
||||
- name: backport to develop
|
||||
conditions:
|
||||
- label="backport develop"
|
||||
|
@ -10,4 +10,42 @@ Entries are:
|
||||
- Sales Invoice (Itemised)
|
||||
- Purchase Invoice (Itemised)
|
||||
|
||||
All accounting entries are stored in the `General Ledger`
|
||||
All accounting entries are stored in the `General Ledger`
|
||||
|
||||
## Payment Ledger
|
||||
Transactions on Receivable and Payable Account types will also be stored in `Payment Ledger`. This is so that payment reconciliation process only requires update on this ledger.
|
||||
|
||||
### Key Fields
|
||||
| Field | Description |
|
||||
|----------------------|----------------------------------|
|
||||
| `account_type` | Receivable/Payable |
|
||||
| `account` | Accounting head |
|
||||
| `party` | Party Name |
|
||||
| `voucher_no` | Voucher No |
|
||||
| `against_voucher_no` | Linked voucher(secondary effect) |
|
||||
| `amount` | can be +ve/-ve |
|
||||
|
||||
### Design
|
||||
`debit` and `credit` have been replaced with `account_type` and `amount`. `against_voucher_no` is populated for all entries. So, outstanding amount can be calculated by summing up amount only using `against_voucher_no`.
|
||||
|
||||
Ex:
|
||||
1. Consider an invoice for ₹100 and a partial payment of ₹80 against that invoice. Payment Ledger will have following entries.
|
||||
|
||||
| voucher_no | against_voucher_no | amount |
|
||||
|------------|--------------------|--------|
|
||||
| SINV-01 | SINV-01 | 100 |
|
||||
| PAY-01 | SINV-01 | -80 |
|
||||
|
||||
|
||||
2. Reconcile a Credit Note against an invoice using a Journal Entry
|
||||
|
||||
An invoice for ₹100 partially reconciled against a credit of ₹70 using a Journal Entry. Payment Ledger will have the following entries.
|
||||
|
||||
| voucher_no | against_voucher_no | amount |
|
||||
|------------|--------------------|--------|
|
||||
| SINV-01 | SINV-01 | 100 |
|
||||
| | | |
|
||||
| CR-NOTE-01 | CR-NOTE-01 | -70 |
|
||||
| | | |
|
||||
| JE-01 | CR-NOTE-01 | +70 |
|
||||
| JE-01 | SINV-01 | -70 |
|
||||
|
@ -322,9 +322,9 @@ def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where is_group = 1 and docstatus != 2 and company = %s
|
||||
and %s like %s order by name limit %s, %s"""
|
||||
and %s like %s order by name limit %s offset %s"""
|
||||
% ("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, start, page_len),
|
||||
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
|
@ -58,16 +58,20 @@ class GLEntry(Document):
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
)
|
||||
if frappe.db.get_value("Account", self.account, "account_type") not in [
|
||||
"Receivable",
|
||||
"Payable",
|
||||
]:
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
)
|
||||
|
||||
def check_mandatory(self):
|
||||
mandatory = ["account", "voucher_type", "voucher_no", "company"]
|
||||
|
@ -149,22 +149,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(frm.doc.voucher_type=="Opening Entry") {
|
||||
return frappe.call({
|
||||
type:"GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
|
||||
args: {
|
||||
"company": frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
frappe.model.clear_table(frm.doc, "accounts");
|
||||
if(r.message) {
|
||||
update_jv_details(frm.doc, r.message);
|
||||
}
|
||||
cur_frm.set_value("is_opening", "Yes");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -137,7 +137,8 @@
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
"options": "Finance Book",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "2_add_edit_gl_entries",
|
||||
@ -538,7 +539,7 @@
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-06 17:18:46.865259",
|
||||
"modified": "2022-06-23 22:01:32.348337",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
@ -400,7 +400,7 @@ class JournalEntry(AccountsController):
|
||||
against_entries = frappe.db.sql(
|
||||
"""select * from `tabJournal Entry Account`
|
||||
where account = %s and docstatus = 1 and parent = %s
|
||||
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
|
||||
and (reference_type is null or reference_type in ('', 'Sales Order', 'Purchase Order'))
|
||||
""",
|
||||
(d.account, d.reference_name),
|
||||
as_dict=True,
|
||||
@ -784,9 +784,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
self.total_amount_in_words = money_in_words(amt, currency)
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit:
|
||||
@ -822,7 +820,12 @@ class JournalEntry(AccountsController):
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
return gl_map
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
gl_map = self.build_gl_map()
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
|
||||
update_outstanding = "No"
|
||||
else:
|
||||
@ -1162,24 +1165,6 @@ def get_payment_entry(ref_doc, args):
|
||||
return je if args.get("journal_entry") else je.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opening_accounts(company):
|
||||
"""get all balance sheet accounts for opening entry"""
|
||||
accounts = frappe.db.sql_list(
|
||||
"""select
|
||||
name from tabAccount
|
||||
where
|
||||
is_group=0 and report_type='Balance Sheet' and company={0} and
|
||||
name not in (select distinct account from tabWarehouse where
|
||||
account is not null and account != '')
|
||||
order by name asc""".format(
|
||||
frappe.db.escape(company)
|
||||
)
|
||||
)
|
||||
|
||||
return [{"account": a, "balance": get_balance_on(a)} for a in accounts]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
@ -1200,7 +1185,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
AND jv.docstatus = 1
|
||||
AND jv.`{0}` LIKE %(txt)s
|
||||
ORDER BY jv.name DESC
|
||||
LIMIT %(offset)s, %(limit)s
|
||||
LIMIT %(limit)s offset %(offset)s
|
||||
""".format(
|
||||
searchfield
|
||||
),
|
||||
|
@ -6,7 +6,7 @@ import json
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, scrub, throw
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
|
||||
import erpnext
|
||||
@ -785,7 +785,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.set("remarks", "\n".join(remarks))
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
def build_gl_map(self):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
|
||||
@ -794,7 +794,10 @@ class PaymentEntry(AccountsController):
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
|
||||
@ -1180,6 +1183,9 @@ def get_outstanding_reference_documents(args):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
common_filter = []
|
||||
|
||||
# confirm that Supplier is not blocked
|
||||
if args.get("party_type") == "Supplier":
|
||||
supplier_status = get_supplier_block_status(args["party"])
|
||||
@ -1201,10 +1207,13 @@ def get_outstanding_reference_documents(args):
|
||||
condition = " and voucher_type={0} and voucher_no={1}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
common_filter.append(ple.voucher_no == args["voucher_no"])
|
||||
|
||||
# Add cost center condition
|
||||
if args.get("cost_center"):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
common_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
date_fields_dict = {
|
||||
"posting_date": ["from_posting_date", "to_posting_date"],
|
||||
@ -1216,16 +1225,19 @@ def get_outstanding_reference_documents(args):
|
||||
condition += " and {0} between '{1}' and '{2}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
common_filter.append(ple[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")))
|
||||
common_filter.append(ple.company == args.get("company"))
|
||||
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
filters=args,
|
||||
condition=condition,
|
||||
common_filter=common_filter,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
@ -1429,7 +1441,7 @@ def get_negative_outstanding_invoices(
|
||||
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())"
|
||||
supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)"
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
rounded_total_field = "base_rounded_total"
|
||||
|
@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
@ -23,7 +24,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
class TestPaymentEntry(unittest.TestCase):
|
||||
class TestPaymentEntry(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_payment_entry_against_order(self):
|
||||
so = make_sales_order()
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:PLE-{YY}-{MM}-{######}",
|
||||
"creation": "2022-05-09 19:35:03.334361",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@ -138,11 +137,10 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-19 18:04:44.609115",
|
||||
"modified": "2022-05-30 19:04:55.532171",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -6,6 +6,19 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import (
|
||||
validate_balance_type,
|
||||
validate_frozen_account,
|
||||
)
|
||||
from erpnext.accounts.utils import update_voucher_outstanding
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
class PaymentLedgerEntry(Document):
|
||||
def validate_account(self):
|
||||
@ -18,5 +31,119 @@ class PaymentLedgerEntry(Document):
|
||||
if not valid_account:
|
||||
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
|
||||
|
||||
def validate_account_details(self):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if ret.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_dimensions(self):
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if self.account == account:
|
||||
if value["is_mandatory"] and not self.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
def validate_dimensions_for_pl_and_bs(self):
|
||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
||||
|
||||
for dimension in get_checks_for_pl_and_bs_accounts():
|
||||
if (
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
self.validate_account_details()
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
# update outstanding amount
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(
|
||||
self.against_voucher_type, self.against_voucher_no, self.account, self.party_type, self.party
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
""" select mode_of_payment from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and mode_of_payment like %(txt)s
|
||||
limit %(start)s, %(page_len)s""",
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
@ -51,7 +51,7 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
""" select supplier from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and supplier like %(txt)s and
|
||||
(payment_reference is null or payment_reference='')
|
||||
limit %(start)s, %(page_len)s""",
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
@ -3,16 +3,26 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unreconciled_entries(self):
|
||||
self.get_nonreconciled_payment_entries()
|
||||
@ -108,54 +118,58 @@ class PaymentReconciliation(Document):
|
||||
return list(journal_entries)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
condition = self.get_conditions(get_return_invoices=True)
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
reconciled_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if dr_or_cr == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
return frappe.db.sql(
|
||||
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
|
||||
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
|
||||
account_currency as currency
|
||||
FROM `tab{doc}` doc, `tabGL Entry` gl
|
||||
WHERE
|
||||
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
|
||||
and doc.{party_type_field} = %(party)s
|
||||
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
|
||||
and gl.against_voucher_type = %(voucher_type)s
|
||||
and doc.docstatus = 1 and gl.party = %(party)s
|
||||
and gl.party_type = %(party_type)s and gl.account = %(account)s
|
||||
and gl.is_cancelled = 0 {condition}
|
||||
GROUP BY doc.name
|
||||
Having
|
||||
amount > 0
|
||||
ORDER BY doc.posting_date
|
||||
""".format(
|
||||
doc=voucher_type,
|
||||
dr_or_cr=dr_or_cr,
|
||||
reconciled_dr_or_cr=reconciled_dr_or_cr,
|
||||
party_type_field=frappe.scrub(self.party_type),
|
||||
condition=condition or "",
|
||||
),
|
||||
{
|
||||
"party": self.party,
|
||||
"party_type": self.party_type,
|
||||
"voucher_type": voucher_type,
|
||||
"account": self.receivable_payable_account,
|
||||
},
|
||||
as_dict=1,
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||
self.common_filter_conditions.append(ple.account_type == "Receivable")
|
||||
else:
|
||||
self.common_filter_conditions.append(ple.account_type == "Payable")
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
# get return invoices
|
||||
doc = qb.DocType(voucher_type)
|
||||
return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
& (IfNull(doc.return_against, "") == "")
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
ple_query = QueryPaymentLedger()
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||
get_payments=True,
|
||||
)
|
||||
|
||||
for inv in return_outstanding:
|
||||
if inv.outstanding != 0:
|
||||
outstanding_dr_or_cr.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_type": inv.voucher_type,
|
||||
"reference_name": inv.voucher_no,
|
||||
"amount": -(inv.outstanding),
|
||||
"posting_date": inv.posting_date,
|
||||
"currency": inv.currency,
|
||||
}
|
||||
)
|
||||
)
|
||||
return outstanding_dr_or_cr
|
||||
|
||||
def add_payment_entries(self, non_reconciled_payments):
|
||||
self.set("payments", [])
|
||||
|
||||
@ -166,10 +180,15 @@ class PaymentReconciliation(Document):
|
||||
def get_invoice_entries(self):
|
||||
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
||||
|
||||
condition = self.get_conditions(get_invoices=True)
|
||||
self.build_qb_filter_conditions(get_invoices=True)
|
||||
|
||||
non_reconciled_invoices = get_outstanding_invoices(
|
||||
self.party_type, self.party, self.receivable_payable_account, condition=condition
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
common_filter=self.common_filter_conditions,
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
)
|
||||
|
||||
if self.invoice_limit:
|
||||
@ -329,89 +348,56 @@ class PaymentReconciliation(Document):
|
||||
if not invoices_to_reconcile:
|
||||
frappe.throw(_("No records found in Allocation table"))
|
||||
|
||||
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
self.common_filter_conditions.clear()
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
self.common_filter_conditions.append(ple.company == self.company)
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_return_invoices):
|
||||
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
|
||||
if self.from_invoice_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
|
||||
if self.to_invoice_date
|
||||
else ""
|
||||
)
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
|
||||
if self.minimum_invoice_amount:
|
||||
condition += " and {dr_or_cr} >= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
|
||||
)
|
||||
if self.maximum_invoice_amount:
|
||||
condition += " and {dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
|
||||
)
|
||||
if self.from_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
|
||||
if self.to_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
|
||||
|
||||
elif get_return_invoices:
|
||||
condition = " and doc.company = '{0}' ".format(self.company)
|
||||
condition += (
|
||||
" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
if self.from_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
|
||||
if self.to_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
|
||||
|
||||
if self.minimum_invoice_amount:
|
||||
condition += " and gl.{dr_or_cr} >= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
|
||||
)
|
||||
if self.maximum_invoice_amount:
|
||||
condition += " and gl.{dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
|
||||
)
|
||||
def get_conditions(self, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
else:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
if self.get("cost_center") and get_payments:
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
|
||||
return condition
|
||||
|
||||
|
@ -4,93 +4,453 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestPaymentReconciliation(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_customer()
|
||||
make_invoice_and_payment()
|
||||
class TestPaymentReconciliation(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
|
||||
def test_payment_reconciliation(self):
|
||||
payment_reco = frappe.get_doc("Payment Reconciliation")
|
||||
payment_reco.company = "_Test Company"
|
||||
payment_reco.party_type = "Customer"
|
||||
payment_reco.party = "_Test Payment Reco Customer"
|
||||
payment_reco.receivable_payable_account = "Debtors - _TC"
|
||||
payment_reco.from_invoice_date = add_days(getdate(), -1)
|
||||
payment_reco.to_invoice_date = getdate()
|
||||
payment_reco.from_payment_date = add_days(getdate(), -1)
|
||||
payment_reco.to_payment_date = getdate()
|
||||
payment_reco.maximum_invoice_amount = 1000
|
||||
payment_reco.maximum_payment_amount = 1000
|
||||
payment_reco.invoice_limit = 10
|
||||
payment_reco.payment_limit = 10
|
||||
payment_reco.bank_cash_account = "_Test Bank - _TC"
|
||||
payment_reco.cost_center = "_Test Cost Center - _TC"
|
||||
payment_reco.get_unreconciled_entries()
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
self.assertEqual(len(payment_reco.get("invoices")), 1)
|
||||
self.assertEqual(len(payment_reco.get("payments")), 1)
|
||||
def create_company(self):
|
||||
company = None
|
||||
if frappe.db.exists("Company", "_Test Payment Reconciliation"):
|
||||
company = frappe.get_doc("Company", "_Test Payment Reconciliation")
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": "_Test Payment Reconciliation",
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
payment_entry = payment_reco.get("payments")[0].reference_name
|
||||
invoice = payment_reco.get("invoices")[0].invoice_number
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses - _PR"
|
||||
self.income_account = "Sales - _PR"
|
||||
self.expense_account = "Cost of Goods Sold - _PR"
|
||||
self.debit_to = "Debtors - _PR"
|
||||
self.creditors = "Creditors - _PR"
|
||||
|
||||
payment_reco.allocate_entries(
|
||||
{
|
||||
"payments": [payment_reco.get("payments")[0].as_dict()],
|
||||
"invoices": [payment_reco.get("invoices")[0].as_dict()],
|
||||
}
|
||||
# create bank account
|
||||
if frappe.db.exists("Account", "HDFC - _PR"):
|
||||
self.bank = "HDFC - _PR"
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - _PR",
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
payment_reco.reconcile()
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
|
||||
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
|
||||
def create_customer(self):
|
||||
if frappe.db.exists("Customer", "_Test PR Customer"):
|
||||
self.customer = "_Test PR Customer"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer"
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
if frappe.db.exists("Customer", "_Test PR Customer 2"):
|
||||
self.customer2 = "_Test PR Customer 2"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer 2"
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer2 = customer.name
|
||||
|
||||
def make_customer():
|
||||
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "_Test Payment Reco Customer",
|
||||
"customer_type": "Individual",
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
}
|
||||
).insert()
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate()):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.bank,
|
||||
paid_amount=amount,
|
||||
)
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def make_invoice_and_payment():
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
|
||||
)
|
||||
si.cost_center = "_Test Cost Center - _TC"
|
||||
si.save()
|
||||
si.submit()
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"payment_type": "Receive",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Payment Reco Customer",
|
||||
"company": "_Test Company",
|
||||
"paid_from_account_currency": "INR",
|
||||
"paid_to_account_currency": "INR",
|
||||
"source_exchange_rate": 1,
|
||||
"target_exchange_rate": 1,
|
||||
"reference_no": "1",
|
||||
"reference_date": getdate(),
|
||||
"received_amount": 690,
|
||||
"paid_amount": 690,
|
||||
"paid_from": "Debtors - _TC",
|
||||
"paid_to": "_Test Bank - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
def create_payment_reconciliation(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
return pr
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
je.user_remark = "test"
|
||||
if not cost_center:
|
||||
cost_center = self.cost_center
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": acc1,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount if amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(amount) if amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": acc2,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount if amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(amount) if amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
return je
|
||||
|
||||
def test_filter_min_max(self):
|
||||
# check filter condition minimum and maximum amount
|
||||
self.create_sales_invoice(qty=1, rate=300)
|
||||
self.create_sales_invoice(qty=1, rate=400)
|
||||
self.create_sales_invoice(qty=1, rate=500)
|
||||
self.create_payment_entry(amount=300).save().submit()
|
||||
self.create_payment_entry(amount=400).save().submit()
|
||||
self.create_payment_entry(amount=500).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.minimum_invoice_amount = 400
|
||||
pr.maximum_invoice_amount = 500
|
||||
pr.minimum_payment_amount = 300
|
||||
pr.maximum_payment_amount = 600
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 3)
|
||||
|
||||
pr.minimum_invoice_amount = 300
|
||||
pr.maximum_invoice_amount = 600
|
||||
pr.minimum_payment_amount = 400
|
||||
pr.maximum_payment_amount = 500
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 3)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
|
||||
pr.minimum_invoice_amount = (
|
||||
pr.maximum_invoice_amount
|
||||
) = pr.minimum_payment_amount = pr.maximum_payment_amount = 0
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 3)
|
||||
self.assertEqual(len(pr.get("payments")), 3)
|
||||
|
||||
def test_filter_posting_date(self):
|
||||
# check filter condition using transaction date
|
||||
date1 = nowdate()
|
||||
date2 = add_days(nowdate(), -1)
|
||||
amount = 100
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=date1)
|
||||
si2 = self.create_sales_invoice(
|
||||
qty=1, rate=amount, posting_date=date2, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si2.set_posting_time = 1
|
||||
si2.posting_date = date2
|
||||
si2.save().submit()
|
||||
self.create_payment_entry(amount=amount, posting_date=date1).save().submit()
|
||||
self.create_payment_entry(amount=amount, posting_date=date2).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = pr.to_invoice_date = date1
|
||||
pr.from_payment_date = pr.to_payment_date = date1
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# assert only si and pe are fetched
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
|
||||
pr.from_invoice_date = date2
|
||||
pr.to_invoice_date = date1
|
||||
pr.from_payment_date = date2
|
||||
pr.to_payment_date = date1
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# assert only si and pe are fetched
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
|
||||
def test_filter_invoice_limit(self):
|
||||
# check filter condition - invoice limit
|
||||
transaction_date = nowdate()
|
||||
rate = 100
|
||||
invoices = []
|
||||
payments = []
|
||||
for i in range(5):
|
||||
invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
|
||||
pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
|
||||
payments.append(pe)
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = pr.to_invoice_date = transaction_date
|
||||
pr.from_payment_date = pr.to_payment_date = transaction_date
|
||||
pr.invoice_limit = 2
|
||||
pr.payment_limit = 3
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 3)
|
||||
|
||||
def test_payment_against_invoice(self):
|
||||
si = self.create_sales_invoice(qty=1, rate=200)
|
||||
pe = self.create_payment_entry(amount=55).save().submit()
|
||||
# second payment entry
|
||||
self.create_payment_entry(amount=35).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
# reconcile multiple payments against invoice
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Partly Paid")
|
||||
# check PR tool output post reconciliation
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 110)
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
# cancel one PE
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
pr.get_unreconciled_entries()
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 165)
|
||||
|
||||
def test_payment_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
|
||||
sales = "Sales - _PR"
|
||||
amount = 921
|
||||
# debit debtors account to record an invoice
|
||||
je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
|
||||
je.accounts[0].party_type = "Customer"
|
||||
je.accounts[0].party = self.customer
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
self.create_payment_entry(amount=amount, posting_date=transaction_date).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
|
||||
pr.from_invoice_date = pr.to_invoice_date = transaction_date
|
||||
pr.from_payment_date = pr.to_payment_date = transaction_date
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
amount = 100
|
||||
|
||||
# debit debtors account to simulate a invoice
|
||||
je1 = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
|
||||
je1.accounts[0].party_type = "Customer"
|
||||
je1.accounts[0].party = self.customer
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
# credit debtors account to simulate a payment
|
||||
je2 = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
def test_cr_note_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# check reconciliation tool output
|
||||
# reconciled invoice and credit note shouldn't show up in selection
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_cr_note_partial_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
allocated_amount = 80
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = allocated_amount
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Partly Paid")
|
||||
self.assertEqual(si.outstanding_amount, 20)
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# check reconciliation tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
|
||||
self.assertEqual(pr.get("payments")[0].amount, 20)
|
||||
|
@ -54,8 +54,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
pce = frappe.db.sql(
|
||||
"""select name from `tabPeriod Closing Voucher`
|
||||
where posting_date > %s and fiscal_year = %s and docstatus = 1""",
|
||||
(self.posting_date, self.fiscal_year),
|
||||
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
|
||||
(self.posting_date, self.fiscal_year, self.company),
|
||||
)
|
||||
if pce and pce[0][0]:
|
||||
frappe.throw(
|
||||
|
@ -173,7 +173,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
where
|
||||
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
|
||||
and (pf.name like %(txt)s)
|
||||
and pf.disabled = 0 limit %(start)s, %(page_len)s""",
|
||||
and pf.disabled = 0 limit %(page_len)s offset %(start)s""",
|
||||
args,
|
||||
)
|
||||
|
||||
|
@ -36,8 +36,12 @@ class PricingRule(Document):
|
||||
|
||||
def validate_duplicate_apply_on(self):
|
||||
if self.apply_on != "Transaction":
|
||||
field = apply_on_dict.get(self.apply_on)
|
||||
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
|
||||
apply_on_table = apply_on_dict.get(self.apply_on)
|
||||
if not apply_on_table:
|
||||
return
|
||||
|
||||
apply_on_field = frappe.scrub(self.apply_on)
|
||||
values = [d.get(apply_on_field) for d in self.get(apply_on_table) if d.get(apply_on_field)]
|
||||
if len(values) != len(set(values)):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
|
@ -165,17 +165,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
super(PurchaseInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
def check_conversion_rate(self):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
if not self.credit_to:
|
||||
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
|
||||
|
@ -1616,6 +1616,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
pi.supplier = "_Test Supplier"
|
||||
pi.company = "_Test Company"
|
||||
pi.append(
|
||||
"items",
|
||||
{
|
||||
"item_name": "Opening item",
|
||||
"qty": 1,
|
||||
"uom": "Tonne",
|
||||
"stock_uom": "Kg",
|
||||
"rate": 1000,
|
||||
"expense_account": "Stock Received But Not Billed - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].conversion_factor, 1000)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
@ -195,6 +195,7 @@
|
||||
"label": "Rejected Qty"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@ -214,6 +215,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "UOM Conversion Factor",
|
||||
@ -222,6 +224,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Accepted Qty in Stock UOM",
|
||||
@ -871,7 +874,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-15 17:04:07.191013",
|
||||
"modified": "2022-06-17 05:31:10.520171",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@ -879,5 +882,6 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1790,6 +1790,8 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"hide_days": 1,
|
||||
@ -2038,7 +2040,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-03-08 16:08:53.517903",
|
||||
"modified": "2022-06-10 03:52:51.409913",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -114,6 +114,7 @@ class SalesInvoice(SellingController):
|
||||
self.set_income_account_for_fixed_assets()
|
||||
self.validate_item_cost_centers()
|
||||
self.validate_income_account()
|
||||
self.check_conversion_rate()
|
||||
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.customer, self.company, self.inter_company_invoice_reference
|
||||
|
@ -792,6 +792,54 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
jv.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0)
|
||||
|
||||
def test_outstanding_on_cost_center_allocation(self):
|
||||
# setup cost centers
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
|
||||
create_cost_center_allocation,
|
||||
)
|
||||
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
# make invoice
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.is_pos = 0
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
# make payment - fully paid
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = si.currency
|
||||
pe.source_exchange_rate = 1
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.cost_center = cca.main_cost_center
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
# cancel cost center allocation
|
||||
cca.cancel()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
|
||||
si = frappe.copy_doc(test_records[1])
|
||||
si.insert()
|
||||
@ -1583,6 +1631,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
|
@ -182,6 +182,7 @@
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@ -200,6 +201,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "UOM Conversion Factor",
|
||||
@ -207,6 +209,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty as per Stock UOM",
|
||||
@ -843,7 +846,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-23 08:18:04.928287",
|
||||
"modified": "2022-06-17 05:33:15.335912",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
@ -145,13 +145,14 @@ class Subscription(Document):
|
||||
You shouldn't need to call this directly. Use `get_billing_cycle` instead.
|
||||
"""
|
||||
plan_names = [plan.plan for plan in self.plans]
|
||||
billing_info = frappe.db.sql(
|
||||
"select distinct `billing_interval`, `billing_interval_count` "
|
||||
"from `tabSubscription Plan` "
|
||||
"where name in %s",
|
||||
(plan_names,),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
subscription_plan = frappe.qb.DocType("Subscription Plan")
|
||||
billing_info = (
|
||||
frappe.qb.from_(subscription_plan)
|
||||
.select(subscription_plan.billing_interval, subscription_plan.billing_interval_count)
|
||||
.distinct()
|
||||
.where(subscription_plan.name.isin(plan_names))
|
||||
).run(as_dict=1)
|
||||
|
||||
return billing_info
|
||||
|
||||
|
@ -35,7 +35,13 @@ def make_gl_entries(
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
create_payment_ledger_entry(gl_map)
|
||||
create_payment_ledger_entry(
|
||||
gl_map,
|
||||
cancel=0,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
@ -126,7 +132,7 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
else:
|
||||
@ -482,6 +488,9 @@ def make_reverse_gl_entries(
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||
create_payment_ledger_entry(
|
||||
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
@ -13,7 +13,7 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2022-06-07 14:29:21.352132",
|
||||
"modified": "2022-06-14 17:38:24.967834",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts",
|
||||
|
@ -2,14 +2,14 @@
|
||||
"action": "Create Entry",
|
||||
"action_label": "Manage Sales Tax Templates",
|
||||
"creation": "2020-05-13 19:29:43.844463",
|
||||
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n",
|
||||
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2022-06-07 14:27:15.906286",
|
||||
"modified": "2022-06-14 17:37:56.694261",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Setup Taxes",
|
||||
"owner": "Administrator",
|
||||
|
@ -211,7 +211,7 @@ def set_address_details(
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]:
|
||||
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]:
|
||||
if party_details.company_address:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "company_address", party_details.company_address)
|
||||
|
@ -42,7 +42,7 @@
|
||||
|
||||
{% if(filters.show_future_payments) { %}
|
||||
{% var balance_row = data.slice(-1).pop();
|
||||
var start = filters.based_on_payment_terms ? 13 : 11;
|
||||
var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
|
||||
var range1 = report.columns[start].label;
|
||||
var range2 = report.columns[start+1].label;
|
||||
var range3 = report.columns[start+2].label;
|
||||
|
@ -172,11 +172,6 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"label": __("Show Sales Person"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"label": __("Tax Id"),
|
||||
|
@ -5,7 +5,9 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@ -41,6 +43,8 @@ def execute(filters=None):
|
||||
class ReceivablePayableReport(object):
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
self.qb_selection_filter = []
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||
self.age_as_on = (
|
||||
getdate(nowdate())
|
||||
@ -78,7 +82,7 @@ class ReceivablePayableReport(object):
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_gl_entries()
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.voucher_balance = OrderedDict()
|
||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||
@ -96,25 +100,25 @@ class ReceivablePayableReport(object):
|
||||
self.get_return_entries()
|
||||
|
||||
self.data = []
|
||||
for gle in self.gl_entries:
|
||||
self.update_voucher_balance(gle)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
|
||||
self.build_data()
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for gle in self.gl_entries:
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
key = (gle.voucher_type, gle.voucher_no, gle.party)
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
if not key in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=gle.voucher_type,
|
||||
voucher_no=gle.voucher_no,
|
||||
party=gle.party,
|
||||
party_account=gle.account,
|
||||
posting_date=gle.posting_date,
|
||||
account_currency=gle.account_currency,
|
||||
remarks=gle.remarks if self.filters.get("show_remarks") else None,
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
@ -124,23 +128,22 @@ class ReceivablePayableReport(object):
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
self.get_invoices(gle)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(gle.party)
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row("Total")
|
||||
|
||||
def get_invoices(self, gle):
|
||||
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
def get_invoices(self, ple):
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if self.filters.get("sales_person"):
|
||||
if gle.voucher_no in self.sales_person_records.get(
|
||||
if ple.voucher_no in self.sales_person_records.get(
|
||||
"Sales Invoice", []
|
||||
) or gle.party in self.sales_person_records.get("Customer", []):
|
||||
self.invoices.add(gle.voucher_no)
|
||||
) or ple.party in self.sales_person_records.get("Customer", []):
|
||||
self.invoices.add(ple.voucher_no)
|
||||
else:
|
||||
self.invoices.add(gle.voucher_no)
|
||||
self.invoices.add(ple.voucher_no)
|
||||
|
||||
def init_subtotal_row(self, party):
|
||||
if not self.total_row_map.get(party):
|
||||
@ -162,39 +165,49 @@ class ReceivablePayableReport(object):
|
||||
"range5",
|
||||
]
|
||||
|
||||
def update_voucher_balance(self, gle):
|
||||
def get_voucher_balance(self, ple):
|
||||
if self.filters.get("sales_person"):
|
||||
if not (
|
||||
ple.party in self.sales_person_records.get("Customer", [])
|
||||
or ple.against_voucher_no in self.sales_person_records.get("Sales Invoice", [])
|
||||
):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
row = self.voucher_balance.get(key)
|
||||
return row
|
||||
|
||||
def update_voucher_balance(self, ple):
|
||||
# get the row where this balance needs to be updated
|
||||
# if its a payment, it will return the linked invoice or will be considered as advance
|
||||
row = self.get_voucher_balance(gle)
|
||||
row = self.get_voucher_balance(ple)
|
||||
if not row:
|
||||
return
|
||||
# gle_balance will be the total "debit - credit" for receivable type reports and
|
||||
# and vice-versa for payable type reports
|
||||
gle_balance = self.get_gle_balance(gle)
|
||||
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
|
||||
|
||||
if gle_balance > 0:
|
||||
if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher:
|
||||
# debit against sales / purchase invoice
|
||||
row.paid -= gle_balance
|
||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
|
||||
# update voucher
|
||||
if ple.amount > 0:
|
||||
if (
|
||||
ple.voucher_type in ["Journal Entry", "Payment Entry"]
|
||||
and ple.voucher_no != ple.against_voucher_no
|
||||
):
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
# invoice
|
||||
row.invoiced += gle_balance
|
||||
row.invoiced_in_account_currency += gle_balance_in_account_currency
|
||||
row.invoiced += amount
|
||||
row.invoiced_in_account_currency += amount_in_account_currency
|
||||
else:
|
||||
# payment or credit note for receivables
|
||||
if self.is_invoice(gle):
|
||||
# stand alone debit / credit note
|
||||
row.credit_note -= gle_balance
|
||||
row.credit_note_in_account_currency -= gle_balance_in_account_currency
|
||||
if self.is_invoice(ple):
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
# advance / unlinked payment or other adjustment
|
||||
row.paid -= gle_balance
|
||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if gle.cost_center:
|
||||
row.cost_center = str(gle.cost_center)
|
||||
if ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
@ -210,39 +223,6 @@ class ReceivablePayableReport(object):
|
||||
self.data.append({})
|
||||
self.update_sub_total_row(sub_total_row, "Total")
|
||||
|
||||
def get_voucher_balance(self, gle):
|
||||
if self.filters.get("sales_person"):
|
||||
against_voucher = gle.against_voucher or gle.voucher_no
|
||||
if not (
|
||||
gle.party in self.sales_person_records.get("Customer", [])
|
||||
or against_voucher in self.sales_person_records.get("Sales Invoice", [])
|
||||
):
|
||||
return
|
||||
|
||||
voucher_balance = None
|
||||
if gle.against_voucher:
|
||||
# find invoice
|
||||
against_voucher = gle.against_voucher
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
# then consider the payment against original sales invoice.
|
||||
if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if gle.against_voucher in self.return_entries:
|
||||
return_against = self.return_entries.get(gle.against_voucher)
|
||||
if return_against:
|
||||
against_voucher = return_against
|
||||
|
||||
voucher_balance = self.voucher_balance.get(
|
||||
(gle.against_voucher_type, against_voucher, gle.party)
|
||||
)
|
||||
|
||||
if not voucher_balance:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
voucher_balance = self.voucher_balance.get((gle.voucher_type, gle.voucher_no, gle.party))
|
||||
|
||||
return voucher_balance
|
||||
|
||||
def build_data(self):
|
||||
# set outstanding for all the accumulated balances
|
||||
# as we can use this to filter out invoices without outstanding
|
||||
@ -260,6 +240,7 @@ class ReceivablePayableReport(object):
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
|
||||
):
|
||||
|
||||
# non-zero oustanding, we must consider this row
|
||||
|
||||
if self.is_invoice(row) and self.filters.based_on_payment_terms:
|
||||
@ -669,48 +650,53 @@ class ReceivablePayableReport(object):
|
||||
index = 4
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_gl_entries(self):
|
||||
def get_ple_entries(self):
|
||||
# get all the GL entries filtered by the given filters
|
||||
|
||||
conditions, values = self.prepare_conditions()
|
||||
order_by = self.get_order_by_condition()
|
||||
self.prepare_conditions()
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
values.insert(2, self.filters.report_date)
|
||||
|
||||
date_condition = """AND (posting_date <= %s
|
||||
OR (against_voucher IS NULL AND DATE(creation) <= %s))"""
|
||||
self.qb_selection_filter.append(
|
||||
(
|
||||
self.ple.posting_date.lte(self.filters.report_date)
|
||||
| (
|
||||
(self.ple.voucher_no == self.ple.against_voucher_no)
|
||||
& (Date(self.ple.creation).lte(self.filters.report_date))
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
date_condition = "AND posting_date <=%s"
|
||||
self.qb_selection_filter.append(self.ple.posting_date.lte(self.filters.report_date))
|
||||
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
|
||||
else:
|
||||
select_fields = "debit, credit"
|
||||
|
||||
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
|
||||
|
||||
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
||||
|
||||
self.gl_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
||||
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
docstatus < 2
|
||||
and is_cancelled = 0
|
||||
and party_type=%s
|
||||
and (party is not null and party != '')
|
||||
{2} {3} {4}""".format(
|
||||
select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks
|
||||
),
|
||||
values,
|
||||
as_dict=True,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.party_type,
|
||||
ple.cost_center,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
ple.amount,
|
||||
ple.amount_in_account_currency,
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.qb_selection_filter))
|
||||
)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_entries = query.run(as_dict=True)
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||
@ -731,23 +717,21 @@ class ReceivablePayableReport(object):
|
||||
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
|
||||
|
||||
def prepare_conditions(self):
|
||||
conditions = [""]
|
||||
values = [self.party_type, self.filters.report_date]
|
||||
self.qb_selection_filter = []
|
||||
party_type_field = scrub(self.party_type)
|
||||
|
||||
self.add_common_filters(conditions, values, party_type_field)
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters(conditions, values)
|
||||
self.add_customer_filters()
|
||||
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters(conditions, values)
|
||||
self.add_supplier_filters()
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions(conditions)
|
||||
self.get_cost_center_conditions()
|
||||
|
||||
self.add_accounting_dimensions_filters(conditions, values)
|
||||
return " and ".join(conditions), values
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self, conditions):
|
||||
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||
@ -755,32 +739,20 @@ class ReceivablePayableReport(object):
|
||||
center.name
|
||||
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
|
||||
]
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
cost_center_string = '", "'.join(cost_center_list)
|
||||
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
|
||||
|
||||
def get_order_by_condition(self):
|
||||
if self.filters.get("group_by_party"):
|
||||
return "order by party, posting_date"
|
||||
else:
|
||||
return "order by posting_date, party"
|
||||
|
||||
def add_common_filters(self, conditions, values, party_type_field):
|
||||
def add_common_filters(self, party_type_field):
|
||||
if self.filters.company:
|
||||
conditions.append("company=%s")
|
||||
values.append(self.filters.company)
|
||||
self.qb_selection_filter.append(self.ple.company == self.filters.company)
|
||||
|
||||
if self.filters.finance_book:
|
||||
conditions.append("ifnull(finance_book, '') in (%s, '')")
|
||||
values.append(self.filters.finance_book)
|
||||
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
|
||||
|
||||
if self.filters.get(party_type_field):
|
||||
conditions.append("party=%s")
|
||||
values.append(self.filters.get(party_type_field))
|
||||
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
|
||||
|
||||
if self.filters.party_account:
|
||||
conditions.append("account =%s")
|
||||
values.append(self.filters.party_account)
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
else:
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
@ -792,46 +764,68 @@ class ReceivablePayableReport(object):
|
||||
]
|
||||
|
||||
if accounts:
|
||||
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts)))
|
||||
values += accounts
|
||||
self.qb_selection_filter.append(self.ple.account.isin(accounts))
|
||||
|
||||
def add_customer_filters(
|
||||
self,
|
||||
):
|
||||
self.customter = qb.DocType("Customer")
|
||||
|
||||
def add_customer_filters(self, conditions, values):
|
||||
if self.filters.get("customer_group"):
|
||||
conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group"))
|
||||
self.get_hierarchical_filters("Customer Group", "customer_group")
|
||||
|
||||
if self.filters.get("territory"):
|
||||
conditions.append(self.get_hierarchical_filters("Territory", "territory"))
|
||||
self.get_hierarchical_filters("Territory", "territory")
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
conditions.append("party in (select name from tabCustomer where payment_terms=%s)")
|
||||
values.append(self.filters.get("payment_terms_template"))
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party_isin(
|
||||
qb.from_(self.customer).where(
|
||||
self.customer.payment_terms == self.filters.get("payment_terms_template")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)")
|
||||
values.append(self.filters.get("sales_partner"))
|
||||
|
||||
def add_supplier_filters(self, conditions, values):
|
||||
if self.filters.get("supplier_group"):
|
||||
conditions.append(
|
||||
"""party in (select name from tabSupplier
|
||||
where supplier_group=%s)"""
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party_isin(
|
||||
qb.from_(self.customer).where(
|
||||
self.customer.default_sales_partner == self.filters.get("payment_terms_template")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def add_supplier_filters(self):
|
||||
supplier = qb.DocType("Supplier")
|
||||
if self.filters.get("supplier_group"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.supplier_group == self.filters.get("supplier_group"))
|
||||
)
|
||||
)
|
||||
values.append(self.filters.get("supplier_group"))
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
conditions.append("party in (select name from tabSupplier where payment_terms=%s)")
|
||||
values.append(self.filters.get("payment_terms_template"))
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.payment_terms == self.filters.get("supplier_group"))
|
||||
)
|
||||
)
|
||||
|
||||
def get_hierarchical_filters(self, doctype, key):
|
||||
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
||||
|
||||
return """party in (select name from tabCustomer
|
||||
where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt}
|
||||
and name=tabCustomer.{key}))""".format(
|
||||
doctype=doctype, lft=lft, rgt=rgt, key=key
|
||||
)
|
||||
doc = qb.DocType(doctype)
|
||||
ple = self.ple
|
||||
customer = self.customer
|
||||
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
|
||||
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
|
||||
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
|
||||
|
||||
def add_accounting_dimensions_filters(self, conditions, values):
|
||||
def add_accounting_dimensions_filters(self):
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
if accounting_dimensions:
|
||||
@ -841,30 +835,16 @@ class ReceivablePayableReport(object):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions.append("{0} in %s".format(dimension.fieldname))
|
||||
values.append(tuple(self.filters.get(dimension.fieldname)))
|
||||
self.qb_selection_filter.append(
|
||||
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
|
||||
)
|
||||
else:
|
||||
self.qb_selection_filter.append(
|
||||
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
|
||||
)
|
||||
|
||||
def get_gle_balance(self, gle):
|
||||
# get the balance of the GL (debit - credit) or reverse balance based on report type
|
||||
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
|
||||
|
||||
def get_gle_balance_in_account_currency(self, gle):
|
||||
# get the balance of the GL (debit - credit) or reverse balance based on report type
|
||||
return gle.get(
|
||||
self.dr_or_cr + "_in_account_currency"
|
||||
) - self.get_reverse_balance_in_account_currency(gle)
|
||||
|
||||
def get_reverse_balance_in_account_currency(self, gle):
|
||||
return gle.get(
|
||||
"debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency"
|
||||
)
|
||||
|
||||
def get_reverse_balance(self, gle):
|
||||
# get "credit" balance if report type is "debit" and vice versa
|
||||
return gle.get("debit" if self.dr_or_cr == "credit" else "credit")
|
||||
|
||||
def is_invoice(self, gle):
|
||||
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
def is_invoice(self, ple):
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
return True
|
||||
|
||||
def get_party_details(self, party):
|
||||
@ -926,9 +906,6 @@ class ReceivablePayableReport(object):
|
||||
width=180,
|
||||
)
|
||||
|
||||
if self.filters.show_remarks:
|
||||
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
|
||||
if self.party_type == "Supplier":
|
||||
|
@ -12,6 +12,7 @@ class TestAccountsReceivable(unittest.TestCase):
|
||||
def test_accounts_receivable(self):
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
|
||||
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
|
@ -43,7 +43,7 @@ def get_columns():
|
||||
"options": "Account",
|
||||
"width": 170,
|
||||
},
|
||||
{"label": _("Amount"), "fieldname": "amount", "width": 120},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
@ -50,7 +50,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
on_change: () => {
|
||||
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
|
||||
let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: year_start_date
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
@ -58,7 +66,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
on_change: () => {
|
||||
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
|
||||
let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
|
||||
frappe.query_report.set_filter_value({
|
||||
period_end_date: year_end_date
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"finance_book",
|
||||
|
@ -425,7 +425,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(totals, "opening", gle)
|
||||
update_value_in_dict(totals, "closing", gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "total", gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
|
||||
|
@ -35,7 +35,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"fieldname":"group_by",
|
||||
"label": __("Group By"),
|
||||
"fieldtype": "Select",
|
||||
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
|
||||
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
|
||||
"default": "Invoice"
|
||||
},
|
||||
],
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, flt, formatdate
|
||||
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
@ -124,6 +124,23 @@ def execute(filters=None):
|
||||
"gross_profit",
|
||||
"gross_profit_percent",
|
||||
],
|
||||
"monthly": [
|
||||
"monthly",
|
||||
"qty",
|
||||
"base_rate",
|
||||
"buying_rate",
|
||||
"base_amount",
|
||||
"buying_amount",
|
||||
"gross_profit",
|
||||
"gross_profit_percent",
|
||||
],
|
||||
"payment_term": [
|
||||
"payment_term",
|
||||
"base_amount",
|
||||
"buying_amount",
|
||||
"gross_profit",
|
||||
"gross_profit_percent",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@ -317,6 +334,19 @@ def get_columns(group_wise_columns, filters):
|
||||
"options": "territory",
|
||||
"width": 100,
|
||||
},
|
||||
"monthly": {
|
||||
"label": _("Monthly"),
|
||||
"fieldname": "monthly",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
"payment_term": {
|
||||
"label": _("Payment Term"),
|
||||
"fieldname": "payment_term",
|
||||
"fieldtype": "Link",
|
||||
"options": "Payment Term",
|
||||
"width": 170,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -390,6 +420,9 @@ class GrossProfitGenerator(object):
|
||||
buying_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.filters.get("group_by") == "Monthly":
|
||||
row.monthly = formatdate(row.posting_date, "MMM YYYY")
|
||||
|
||||
if self.skip_row(row):
|
||||
continue
|
||||
|
||||
@ -445,17 +478,7 @@ class GrossProfitGenerator(object):
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
for key in list(self.grouped):
|
||||
if self.filters.get("group_by") != "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if i == 0:
|
||||
new_row = row
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
if self.filters.get("group_by") == "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if row.indent == 1.0:
|
||||
if (
|
||||
@ -469,6 +492,44 @@ class GrossProfitGenerator(object):
|
||||
if flt(row.qty) or row.base_amount:
|
||||
row = self.set_average_rate(row)
|
||||
self.grouped_data.append(row)
|
||||
elif self.filters.get("group_by") == "Payment Term":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
invoice_portion = 0
|
||||
|
||||
if row.is_return:
|
||||
invoice_portion = 100
|
||||
elif row.invoice_portion:
|
||||
invoice_portion = row.invoice_portion
|
||||
else:
|
||||
invoice_portion = row.payment_amount * 100 / row.base_net_amount
|
||||
|
||||
if i == 0:
|
||||
new_row = row
|
||||
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if i == 0:
|
||||
new_row = row
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
|
||||
def set_average_based_on_payment_term_portion(self, new_row, row, invoice_portion, aggr=False):
|
||||
cols = ["base_amount", "buying_amount", "gross_profit"]
|
||||
for col in cols:
|
||||
if aggr:
|
||||
new_row[col] += row[col] * invoice_portion / 100
|
||||
else:
|
||||
new_row[col] = row[col] * invoice_portion / 100
|
||||
|
||||
def is_not_invoice_row(self, row):
|
||||
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
|
||||
@ -622,6 +683,20 @@ class GrossProfitGenerator(object):
|
||||
sales_person_cols = ""
|
||||
sales_team_table = ""
|
||||
|
||||
if self.filters.group_by == "Payment Term":
|
||||
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
||||
'{0}',
|
||||
coalesce(schedule.payment_term, '{1}')) as payment_term,
|
||||
schedule.invoice_portion,
|
||||
schedule.payment_amount """.format(
|
||||
_("Sales Return"), _("No Terms")
|
||||
)
|
||||
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
||||
`tabSales Invoice`.is_return = 0 """
|
||||
else:
|
||||
payment_term_cols = ""
|
||||
payment_term_table = ""
|
||||
|
||||
if self.filters.get("sales_invoice"):
|
||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
||||
|
||||
@ -644,10 +719,12 @@ class GrossProfitGenerator(object):
|
||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
||||
`tabSales Invoice Item`.cost_center
|
||||
{sales_person_cols}
|
||||
{payment_term_cols}
|
||||
from
|
||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||
{sales_team_table}
|
||||
{payment_term_table}
|
||||
where
|
||||
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
||||
order by
|
||||
@ -655,6 +732,8 @@ class GrossProfitGenerator(object):
|
||||
conditions=conditions,
|
||||
sales_person_cols=sales_person_cols,
|
||||
sales_team_table=sales_team_table,
|
||||
payment_term_cols=payment_term_cols,
|
||||
payment_term_table=payment_term_table,
|
||||
match_cond=get_match_cond("Sales Invoice"),
|
||||
),
|
||||
self.filters,
|
||||
|
@ -100,7 +100,7 @@ def get_sales_details(filters):
|
||||
sales_data = frappe.db.sql(
|
||||
"""
|
||||
select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date,
|
||||
DATEDIFF(CURDATE(), {date_field}) as days_since_last_order
|
||||
DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order
|
||||
from `tab{doctype}` s, `tab{doctype} Item` si
|
||||
where s.name = si.parent and s.docstatus = 1
|
||||
order by days_since_last_order """.format( # nosec
|
||||
|
@ -179,7 +179,7 @@ def get_sales_invoice_data(filters):
|
||||
def get_mode_of_payments(filters):
|
||||
mode_of_payments = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list)
|
||||
invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop = frappe.db.sql(
|
||||
"""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
|
||||
@ -200,7 +200,7 @@ def get_mode_of_payments(filters):
|
||||
from `tabJournal Entry` a, `tabJournal Entry Account` b
|
||||
where a.name = b.parent
|
||||
and a.docstatus = 1
|
||||
and b.reference_type = "Sales Invoice"
|
||||
and b.reference_type = 'Sales Invoice'
|
||||
and b.reference_name in ({invoice_list_names})
|
||||
""".format(
|
||||
invoice_list_names=invoice_list_names
|
||||
@ -228,7 +228,7 @@ def get_invoices(filters):
|
||||
def get_mode_of_payment_details(filters):
|
||||
mode_of_payment_details = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list)
|
||||
invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop_detail = frappe.db.sql(
|
||||
"""
|
||||
@ -259,7 +259,7 @@ def get_mode_of_payment_details(filters):
|
||||
from `tabJournal Entry` a, `tabJournal Entry Account` b
|
||||
where a.name = b.parent
|
||||
and a.docstatus = 1
|
||||
and b.reference_type = "Sales Invoice"
|
||||
and b.reference_type = 'Sales Invoice'
|
||||
and b.reference_name in ({invoice_list_names})
|
||||
group by a.owner, a.posting_date, mode_of_payment
|
||||
) t
|
||||
|
@ -160,14 +160,12 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
if filters.project:
|
||||
additional_conditions += " and project = %(project)s"
|
||||
|
||||
if filters.finance_book:
|
||||
fb_conditions = " AND finance_book = %(finance_book)s"
|
||||
if filters.include_default_book_entries:
|
||||
fb_conditions = (
|
||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
|
||||
additional_conditions += fb_conditions
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
|
@ -62,8 +62,8 @@ class TestUtils(unittest.TestCase):
|
||||
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
|
||||
|
||||
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
|
||||
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
|
||||
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
|
||||
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
|
||||
|
||||
for doc in (se1, se2, se3):
|
||||
vouchers.append((doc.doctype, doc.name))
|
||||
|
@ -3,13 +3,28 @@
|
||||
|
||||
|
||||
from json import loads
|
||||
from typing import List, Tuple
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
create_batch,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_number_format_info,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
)
|
||||
from pypika import Order
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
import erpnext
|
||||
|
||||
@ -19,6 +34,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
|
||||
|
||||
|
||||
class FiscalYearError(frappe.ValidationError):
|
||||
pass
|
||||
@ -28,6 +46,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
GL_REPOSTING_CHUNK = 100
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fiscal_year(
|
||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
|
||||
@ -42,37 +63,32 @@ def get_fiscal_years(
|
||||
|
||||
if not fiscal_years:
|
||||
# if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
|
||||
cond = ""
|
||||
if fiscal_year:
|
||||
cond += " and fy.name = {0}".format(frappe.db.escape(fiscal_year))
|
||||
if company:
|
||||
cond += """
|
||||
and (not exists (select name
|
||||
from `tabFiscal Year Company` fyc
|
||||
where fyc.parent = fy.name)
|
||||
or exists(select company
|
||||
from `tabFiscal Year Company` fyc
|
||||
where fyc.parent = fy.name
|
||||
and fyc.company=%(company)s)
|
||||
)
|
||||
"""
|
||||
FY = DocType("Fiscal Year")
|
||||
|
||||
fiscal_years = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
fy.name, fy.year_start_date, fy.year_end_date
|
||||
from
|
||||
`tabFiscal Year` fy
|
||||
where
|
||||
disabled = 0 {0}
|
||||
order by
|
||||
fy.year_start_date desc""".format(
|
||||
cond
|
||||
),
|
||||
{"company": company},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(FY)
|
||||
.select(FY.name, FY.year_start_date, FY.year_end_date)
|
||||
.where(FY.disabled == 0)
|
||||
)
|
||||
|
||||
if fiscal_year:
|
||||
query = query.where(FY.name == fiscal_year)
|
||||
|
||||
if company:
|
||||
FYC = DocType("Fiscal Year Company")
|
||||
query = query.where(
|
||||
ExistsCriterion(frappe.qb.from_(FYC).select(FYC.name).where(FYC.parent == FY.name)).negate()
|
||||
| ExistsCriterion(
|
||||
frappe.qb.from_(FYC)
|
||||
.select(FYC.company)
|
||||
.where(FYC.parent == FY.name)
|
||||
.where(FYC.company == company)
|
||||
)
|
||||
)
|
||||
|
||||
query = query.orderby(FY.year_start_date, Order.desc)
|
||||
fiscal_years = query.run(as_dict=True)
|
||||
|
||||
frappe.cache().hset("fiscal_years", company, fiscal_years)
|
||||
|
||||
if not transaction_date and not fiscal_year:
|
||||
@ -423,7 +439,8 @@ def reconcile_against_document(args):
|
||||
# cancel advance entry
|
||||
doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
frappe.flags.ignore_party_validation = True
|
||||
doc.make_gl_entries(cancel=1, adv_adj=1)
|
||||
gl_map = doc.build_gl_map()
|
||||
create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
|
||||
|
||||
for entry in entries:
|
||||
check_if_advance_entry_modified(entry)
|
||||
@ -438,7 +455,9 @@ def reconcile_against_document(args):
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
doc.make_gl_entries(cancel=0, adv_adj=1)
|
||||
gl_map = doc.build_gl_map()
|
||||
create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1)
|
||||
|
||||
frappe.flags.ignore_party_validation = False
|
||||
|
||||
if entry.voucher_type in ("Payment Entry", "Journal Entry"):
|
||||
@ -461,7 +480,7 @@ def check_if_advance_entry_modified(args):
|
||||
select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where t1.name = t2.parent and t2.account = %(account)s
|
||||
and t2.party_type = %(party_type)s and t2.party = %(party)s
|
||||
and (t2.reference_type is null or t2.reference_type in ("", "Sales Order", "Purchase Order"))
|
||||
and (t2.reference_type is null or t2.reference_type in ('', 'Sales Order', 'Purchase Order'))
|
||||
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
|
||||
and t1.docstatus=1 """.format(
|
||||
dr_or_cr=args.get("dr_or_cr")
|
||||
@ -481,7 +500,7 @@ def check_if_advance_entry_modified(args):
|
||||
t1.name = t2.parent and t1.docstatus = 1
|
||||
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
|
||||
and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s
|
||||
and t2.reference_doctype in ("", "Sales Order", "Purchase Order")
|
||||
and t2.reference_doctype in ('', 'Sales Order', 'Purchase Order')
|
||||
and t2.allocated_amount = %(unreconciled_amount)s
|
||||
""".format(
|
||||
party_account_field
|
||||
@ -802,7 +821,11 @@ def get_held_invoices(party_type, party):
|
||||
return held_invoices
|
||||
|
||||
|
||||
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
|
||||
def get_outstanding_invoices(
|
||||
party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
outstanding_invoices = []
|
||||
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
|
||||
|
||||
@ -815,76 +838,30 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
|
||||
else:
|
||||
party_account_type = erpnext.get_party_account_type(party_type)
|
||||
|
||||
if party_account_type == "Receivable":
|
||||
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
|
||||
else:
|
||||
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
|
||||
payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
|
||||
held_invoices = get_held_invoices(party_type, party)
|
||||
|
||||
invoice_list = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
voucher_no, voucher_type, posting_date, due_date,
|
||||
ifnull(sum({dr_or_cr}), 0) as invoice_amount,
|
||||
account_currency as currency
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
party_type = %(party_type)s and party = %(party)s
|
||||
and account = %(account)s and {dr_or_cr} > 0
|
||||
and is_cancelled=0
|
||||
{condition}
|
||||
and ((voucher_type = 'Journal Entry'
|
||||
and (against_voucher = '' or against_voucher is null))
|
||||
or (voucher_type not in ('Journal Entry', 'Payment Entry')))
|
||||
group by voucher_type, voucher_no
|
||||
order by posting_date, name""".format(
|
||||
dr_or_cr=dr_or_cr, condition=condition or ""
|
||||
),
|
||||
{
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"account": account,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
common_filter = common_filter or []
|
||||
common_filter.append(ple.account_type == party_account_type)
|
||||
common_filter.append(ple.account == account)
|
||||
common_filter.append(ple.party_type == party_type)
|
||||
common_filter.append(ple.party == party)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select against_voucher_type, against_voucher,
|
||||
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
|
||||
from `tabGL Entry`
|
||||
where party_type = %(party_type)s and party = %(party)s
|
||||
and account = %(account)s
|
||||
and {payment_dr_or_cr} > 0
|
||||
and against_voucher is not null and against_voucher != ''
|
||||
and is_cancelled=0
|
||||
group by against_voucher_type, against_voucher
|
||||
""".format(
|
||||
payment_dr_or_cr=payment_dr_or_cr
|
||||
),
|
||||
{"party_type": party_type, "party": party, "account": account},
|
||||
as_dict=True,
|
||||
ple_query = QueryPaymentLedger()
|
||||
invoice_list = ple_query.get_voucher_outstandings(
|
||||
common_filter=common_filter,
|
||||
min_outstanding=min_outstanding,
|
||||
max_outstanding=max_outstanding,
|
||||
get_invoices=True,
|
||||
)
|
||||
|
||||
pe_map = frappe._dict()
|
||||
for d in payment_entries:
|
||||
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
|
||||
|
||||
for d in invoice_list:
|
||||
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
|
||||
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
|
||||
payment_amount = d.invoice_amount - d.outstanding
|
||||
outstanding_amount = d.outstanding
|
||||
if outstanding_amount > 0.5 / (10**precision):
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and not (
|
||||
outstanding_amount >= filters.get("outstanding_amt_greater_than")
|
||||
and outstanding_amount <= filters.get("outstanding_amt_less_than")
|
||||
)
|
||||
min_outstanding
|
||||
and max_outstanding
|
||||
and not (outstanding_amount >= min_outstanding and outstanding_amount <= max_outstanding)
|
||||
):
|
||||
continue
|
||||
|
||||
@ -1122,38 +1099,62 @@ def update_gl_entries_after(
|
||||
|
||||
|
||||
def repost_gle_for_stock_vouchers(
|
||||
stock_vouchers, posting_date, company=None, warehouse_account=None
|
||||
stock_vouchers: List[Tuple[str, str]],
|
||||
posting_date: str,
|
||||
company: Optional[str] = None,
|
||||
warehouse_account=None,
|
||||
repost_doc: Optional["RepostItemValuation"] = None,
|
||||
):
|
||||
|
||||
from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
|
||||
|
||||
if not stock_vouchers:
|
||||
return
|
||||
|
||||
def _delete_gl_entries(voucher_type, voucher_no):
|
||||
frappe.db.sql(
|
||||
"""delete from `tabGL Entry`
|
||||
where voucher_type=%s and voucher_no=%s""",
|
||||
(voucher_type, voucher_no),
|
||||
)
|
||||
|
||||
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
||||
|
||||
if not warehouse_account:
|
||||
warehouse_account = get_warehouse_account_map(company)
|
||||
|
||||
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
||||
if repost_doc and repost_doc.gl_reposting_index:
|
||||
# Restore progress
|
||||
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
|
||||
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
||||
|
||||
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
|
||||
for voucher_type, voucher_no in stock_vouchers:
|
||||
existing_gle = gle.get((voucher_type, voucher_no), [])
|
||||
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
|
||||
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
|
||||
if expected_gle:
|
||||
if not existing_gle or not compare_existing_and_expected_gle(
|
||||
existing_gle, expected_gle, precision
|
||||
):
|
||||
for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK):
|
||||
gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
|
||||
|
||||
for voucher_type, voucher_no in stock_vouchers_chunk:
|
||||
existing_gle = gle.get((voucher_type, voucher_no), [])
|
||||
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
|
||||
# Some transactions post credit as negative debit, this is handled while posting GLE
|
||||
# but while comparing we need to make sure it's flipped so comparisons are accurate
|
||||
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
|
||||
if expected_gle:
|
||||
if not existing_gle or not compare_existing_and_expected_gle(
|
||||
existing_gle, expected_gle, precision
|
||||
):
|
||||
_delete_gl_entries(voucher_type, voucher_no)
|
||||
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
||||
else:
|
||||
_delete_gl_entries(voucher_type, voucher_no)
|
||||
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
||||
else:
|
||||
_delete_gl_entries(voucher_type, voucher_no)
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
if repost_doc:
|
||||
repost_doc.db_set(
|
||||
"gl_reposting_index",
|
||||
cint(repost_doc.gl_reposting_index) + len(stock_vouchers_chunk),
|
||||
)
|
||||
|
||||
|
||||
def _delete_gl_entries(voucher_type, voucher_no):
|
||||
frappe.db.sql(
|
||||
"""delete from `tabGL Entry`
|
||||
where voucher_type=%s and voucher_no=%s""",
|
||||
(voucher_type, voucher_no),
|
||||
)
|
||||
|
||||
|
||||
def sort_stock_vouchers_by_posting_date(
|
||||
@ -1167,6 +1168,9 @@ def sort_stock_vouchers_by_posting_date(
|
||||
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
||||
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
||||
.groupby(sle.voucher_type, sle.voucher_no)
|
||||
.orderby(sle.posting_date)
|
||||
.orderby(sle.posting_time)
|
||||
.orderby(sle.creation)
|
||||
).run(as_dict=True)
|
||||
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||
|
||||
@ -1348,7 +1352,9 @@ def check_and_delete_linked_reports(report):
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
def create_payment_ledger_entry(
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
|
||||
):
|
||||
if gl_entries:
|
||||
ple = None
|
||||
|
||||
@ -1421,9 +1427,42 @@ def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.flags.adv_adj = adv_adj
|
||||
ple.flags.from_repost = from_repost
|
||||
ple.flags.update_outstanding = update_outstanding
|
||||
ple.submit()
|
||||
|
||||
|
||||
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
|
||||
common_filter = []
|
||||
if account:
|
||||
common_filter.append(ple.account == account)
|
||||
|
||||
if party_type:
|
||||
common_filter.append(ple.party_type == party_type)
|
||||
|
||||
if party:
|
||||
common_filter.append(ple.party == party)
|
||||
|
||||
ple_query = QueryPaymentLedger()
|
||||
|
||||
# on cancellation outstanding can be an empty list
|
||||
voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
|
||||
if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding:
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"]
|
||||
frappe.db.set_value(
|
||||
voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"]
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry):
|
||||
if pl_entry:
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@ -1445,3 +1484,196 @@ def delink_original_entry(pl_entry):
|
||||
)
|
||||
)
|
||||
query.run()
|
||||
|
||||
|
||||
class QueryPaymentLedger(object):
|
||||
"""
|
||||
Helper Class for Querying Payment Ledger Entry
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
# query result
|
||||
self.voucher_outstandings = []
|
||||
|
||||
# query filters
|
||||
self.vouchers = []
|
||||
self.common_filter = []
|
||||
self.min_outstanding = None
|
||||
self.max_outstanding = None
|
||||
|
||||
def reset(self):
|
||||
# clear filters
|
||||
self.vouchers.clear()
|
||||
self.common_filter.clear()
|
||||
self.min_outstanding = self.max_outstanding = None
|
||||
|
||||
# clear result
|
||||
self.voucher_outstandings.clear()
|
||||
|
||||
def query_for_outstanding(self):
|
||||
"""
|
||||
Database query to fetch voucher amount and voucher outstanding using Common Table Expression
|
||||
"""
|
||||
|
||||
ple = self.ple
|
||||
|
||||
filter_on_voucher_no = []
|
||||
filter_on_against_voucher_no = []
|
||||
if self.vouchers:
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
voucher_nos = set([x.voucher_no for x in self.vouchers])
|
||||
|
||||
filter_on_voucher_no.append(ple.voucher_type.isin(voucher_types))
|
||||
filter_on_voucher_no.append(ple.voucher_no.isin(voucher_nos))
|
||||
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
|
||||
|
||||
# build outstanding amount filter
|
||||
filter_on_outstanding_amount = []
|
||||
if self.min_outstanding:
|
||||
if self.min_outstanding > 0:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency >= self.min_outstanding
|
||||
)
|
||||
else:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency <= self.min_outstanding
|
||||
)
|
||||
if self.max_outstanding:
|
||||
if self.max_outstanding > 0:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency <= self.max_outstanding
|
||||
)
|
||||
else:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency >= self.max_outstanding
|
||||
)
|
||||
|
||||
# build query for voucher amount
|
||||
query_voucher_amount = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
|
||||
)
|
||||
|
||||
# build query for voucher outstanding
|
||||
query_voucher_outstanding = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.against_voucher_type.as_("voucher_type"),
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_against_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
||||
)
|
||||
|
||||
# build CTE for combining voucher amount and outstanding
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
qb.with_(query_voucher_amount, "vouchers")
|
||||
.with_(query_voucher_outstanding, "outstanding")
|
||||
.from_(AliasedQuery("vouchers"))
|
||||
.left_join(AliasedQuery("outstanding"))
|
||||
.on(
|
||||
(AliasedQuery("vouchers").account == AliasedQuery("outstanding").account)
|
||||
& (AliasedQuery("vouchers").voucher_type == AliasedQuery("outstanding").voucher_type)
|
||||
& (AliasedQuery("vouchers").voucher_no == AliasedQuery("outstanding").voucher_no)
|
||||
& (AliasedQuery("vouchers").party_type == AliasedQuery("outstanding").party_type)
|
||||
& (AliasedQuery("vouchers").party == AliasedQuery("outstanding").party)
|
||||
)
|
||||
.select(
|
||||
Table("vouchers").account,
|
||||
Table("vouchers").voucher_type,
|
||||
Table("vouchers").voucher_no,
|
||||
Table("vouchers").party_type,
|
||||
Table("vouchers").party,
|
||||
Table("vouchers").posting_date,
|
||||
Table("vouchers").amount.as_("invoice_amount"),
|
||||
Table("vouchers").amount_in_account_currency.as_("invoice_amount_in_account_currency"),
|
||||
Table("outstanding").amount.as_("outstanding"),
|
||||
Table("outstanding").amount_in_account_currency.as_("outstanding_in_account_currency"),
|
||||
(Table("vouchers").amount - Table("outstanding").amount).as_("paid_amount"),
|
||||
(
|
||||
Table("vouchers").amount_in_account_currency - Table("outstanding").amount_in_account_currency
|
||||
).as_("paid_amount_in_account_currency"),
|
||||
Table("vouchers").due_date,
|
||||
Table("vouchers").currency,
|
||||
)
|
||||
.where(Criterion.all(filter_on_outstanding_amount))
|
||||
)
|
||||
|
||||
# build CTE filter
|
||||
# only fetch invoices
|
||||
if self.get_invoices:
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
self.cte_query_voucher_amount_and_outstanding.having(
|
||||
qb.Field("outstanding_in_account_currency") > 0
|
||||
)
|
||||
)
|
||||
# only fetch payments
|
||||
elif self.get_payments:
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
self.cte_query_voucher_amount_and_outstanding.having(
|
||||
qb.Field("outstanding_in_account_currency") < 0
|
||||
)
|
||||
)
|
||||
|
||||
# execute SQL
|
||||
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
|
||||
|
||||
def get_voucher_outstandings(
|
||||
self,
|
||||
vouchers=None,
|
||||
common_filter=None,
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
get_payments=False,
|
||||
get_invoices=False,
|
||||
):
|
||||
"""
|
||||
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
|
||||
|
||||
vouchers - dict of vouchers to get
|
||||
common_filter - array of criterions
|
||||
min_outstanding - filter on minimum total outstanding amount
|
||||
max_outstanding - filter on maximum total outstanding amount
|
||||
get_invoices - only fetch vouchers(ledger entries with +ve outstanding)
|
||||
get_payments - only fetch payments(ledger entries with -ve outstanding)
|
||||
"""
|
||||
|
||||
self.reset()
|
||||
self.vouchers = vouchers
|
||||
self.common_filter = common_filter or []
|
||||
self.min_outstanding = min_outstanding
|
||||
self.max_outstanding = max_outstanding
|
||||
self.get_payments = get_payments
|
||||
self.get_invoices = get_invoices
|
||||
self.query_for_outstanding()
|
||||
|
||||
return self.voucher_outstandings
|
||||
|
@ -504,18 +504,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "GL Entry",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "DATEV Export",
|
||||
"link_count": 0,
|
||||
"link_to": "DATEV",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "Germany",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "GL Entry",
|
||||
"hidden": 0,
|
||||
@ -1024,16 +1012,16 @@
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Cost Center",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Cost Center Allocation",
|
||||
"link_count": 0,
|
||||
"link_to": "Cost Center Allocation",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
"dependencies": "Cost Center",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Cost Center Allocation",
|
||||
"link_count": 0,
|
||||
"link_to": "Cost Center Allocation",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Cost Center",
|
||||
"hidden": 0,
|
||||
@ -1235,13 +1223,14 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-01-13 17:25:09.835345",
|
||||
"modified": "2022-06-10 15:49:42.990860",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 2.0,
|
||||
|
@ -252,6 +252,7 @@ class Asset(AccountsController):
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
skip_row = False
|
||||
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
|
||||
|
||||
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
@ -265,6 +266,9 @@ class Asset(AccountsController):
|
||||
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if should_get_last_day:
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
@ -849,14 +853,9 @@ class Asset(AccountsController):
|
||||
if args.get("rate_of_depreciation") and on_validate:
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
no_of_years = (
|
||||
flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation")))
|
||||
/ 12
|
||||
)
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
|
||||
# square root of flt(salvage_value) / flt(asset_cost)
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(no_of_years, 2))
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
|
||||
return 100 * (1 - flt(depreciation_rate, float_precision))
|
||||
|
||||
@ -1105,9 +1104,18 @@ def is_cwip_accounting_enabled(asset_category):
|
||||
def get_total_days(date, frequency):
|
||||
period_start_date = add_months(date, cint(frequency) * -1)
|
||||
|
||||
if is_last_day_of_the_month(date):
|
||||
period_start_date = get_last_day(period_start_date)
|
||||
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
|
@ -707,6 +707,39 @@ class TestDepreciationMethods(AssetSetup):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_monthly_depreciation_by_wdv_method(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2022-02-15",
|
||||
purchase_date="2022-02-15",
|
||||
depreciation_method="Written Down Value",
|
||||
gross_purchase_amount=10000,
|
||||
expected_value_after_useful_life=5000,
|
||||
depreciation_start_date="2022-02-28",
|
||||
total_number_of_depreciations=5,
|
||||
frequency_of_depreciation=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2022-02-28", 645.0, 645.0],
|
||||
["2022-03-31", 1206.8, 1851.8],
|
||||
["2022-04-30", 1051.12, 2902.92],
|
||||
["2022-05-31", 915.52, 3818.44],
|
||||
["2022-06-30", 797.42, 4615.86],
|
||||
["2022-07-15", 384.14, 5000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[
|
||||
cstr(d.schedule_date),
|
||||
flt(d.depreciation_amount, 2),
|
||||
flt(d.accumulated_depreciation_amount, 2),
|
||||
]
|
||||
for d in asset.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_discounted_wdv_depreciation_rate_for_indian_region(self):
|
||||
# set indian company
|
||||
company_flag = frappe.flags.company
|
||||
@ -838,7 +871,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(expected_values[i][0], schedule.schedule_date)
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
|
||||
def test_set_accumulated_depreciation(self):
|
||||
@ -1333,6 +1366,32 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.cost_center = "Main - _TC"
|
||||
asset.submit()
|
||||
|
||||
def test_depreciation_on_final_day_of_the_month(self):
|
||||
"""Tests if final day of the month is picked each time, if the depreciation start date is the last day of the month."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-02-15",
|
||||
depreciation_start_date="2020-02-29",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=5,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_dates = [
|
||||
"2020-02-29",
|
||||
"2020-03-31",
|
||||
"2020-04-30",
|
||||
"2020-05-31",
|
||||
"2020-06-30",
|
||||
"2020-07-15",
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
|
@ -47,17 +47,19 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
|
||||
team_member = frappe.db.get_value("User", assign_to_member, "email")
|
||||
args = {
|
||||
"doctype": "Asset Maintenance",
|
||||
"assign_to": [team_member],
|
||||
"assign_to": team_member,
|
||||
"name": asset_maintenance_name,
|
||||
"description": maintenance_task,
|
||||
"date": next_due_date,
|
||||
}
|
||||
if not frappe.db.sql(
|
||||
"""select owner from `tabToDo`
|
||||
where reference_type=%(doctype)s and reference_name=%(name)s and status="Open"
|
||||
where reference_type=%(doctype)s and reference_name=%(name)s and status='Open'
|
||||
and owner=%(assign_to)s""",
|
||||
args,
|
||||
):
|
||||
# assign_to function expects a list
|
||||
args["assign_to"] = [args["assign_to"]]
|
||||
assign_to.add(args)
|
||||
|
||||
|
||||
|
@ -140,6 +140,43 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# ordered qty decreases as ordered qty is 0 (deleted row)
|
||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
|
||||
|
||||
def test_supplied_items_validations_on_po_update_after_submit(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100)
|
||||
item = po.items[0]
|
||||
|
||||
original_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
|
||||
# Just update rate
|
||||
trans_item = [
|
||||
{
|
||||
"item_code": "_Test FG Item",
|
||||
"rate": 20,
|
||||
"qty": 5,
|
||||
"conversion_factor": 1.0,
|
||||
"docname": item.name,
|
||||
}
|
||||
]
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys()))
|
||||
|
||||
# Update qty to 2x
|
||||
trans_item[0]["qty"] *= 2
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values()))
|
||||
|
||||
# Set transfer qty and attempt to update qty, shouldn't be allowed
|
||||
po.supplied_items[0].supplied_qty = 2
|
||||
po.supplied_items[0].db_update()
|
||||
trans_item[0]["qty"] *= 2
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
|
||||
def test_update_child(self):
|
||||
mr = make_material_request(qty=10)
|
||||
po = make_purchase_order(mr.name)
|
||||
@ -330,7 +367,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
else:
|
||||
# update valid from
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabItem Tax` set valid_from = CURDATE()
|
||||
"""UPDATE `tabItem Tax` set valid_from = CURRENT_DATE
|
||||
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||
{"item": item, "tax": tax_template},
|
||||
)
|
||||
|
@ -213,6 +213,7 @@
|
||||
"width": "60px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@ -242,6 +243,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "UOM Conversion Factor",
|
||||
@ -593,6 +595,7 @@
|
||||
"label": "Billed, Received & Returned"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty in Stock UOM",
|
||||
@ -851,7 +854,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-02 13:10:18.398976",
|
||||
"modified": "2022-06-17 05:29:40.602349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -285,7 +285,7 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters):
|
||||
"""select `tabContact`.name from `tabContact`, `tabDynamic Link`
|
||||
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s
|
||||
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
|
||||
limit %(start)s, %(page_len)s""",
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
|
||||
)
|
||||
|
||||
|
@ -84,6 +84,9 @@ class Supplier(TransactionBase):
|
||||
self.save()
|
||||
|
||||
def validate_internal_supplier(self):
|
||||
if not self.is_internal_supplier:
|
||||
self.represents_company = ""
|
||||
|
||||
internal_supplier = frappe.db.get_value(
|
||||
"Supplier",
|
||||
{
|
||||
|
@ -252,7 +252,7 @@ def get_mapped_pi_records():
|
||||
ON pi_item.`purchase_order` = po.`name`
|
||||
WHERE
|
||||
pi_item.docstatus = 1
|
||||
AND po.status not in ("Closed","Completed","Cancelled")
|
||||
AND po.status not in ('Closed','Completed','Cancelled')
|
||||
AND pi_item.po_detail IS NOT NULL
|
||||
"""
|
||||
)
|
||||
@ -271,7 +271,7 @@ def get_mapped_pr_records():
|
||||
pr.docstatus=1
|
||||
AND pr.name=pr_item.parent
|
||||
AND pr_item.purchase_order_item IS NOT NULL
|
||||
AND pr.status not in ("Closed","Completed","Cancelled")
|
||||
AND pr.status not in ('Closed','Completed','Cancelled')
|
||||
"""
|
||||
)
|
||||
)
|
||||
@ -302,7 +302,7 @@ def get_po_entries(conditions):
|
||||
WHERE
|
||||
parent.docstatus = 1
|
||||
AND parent.name = child.parent
|
||||
AND parent.status not in ("Closed","Completed","Cancelled")
|
||||
AND parent.status not in ('Closed','Completed','Cancelled')
|
||||
{conditions}
|
||||
GROUP BY
|
||||
parent.name, child.item_code
|
||||
|
@ -59,6 +59,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
for (let option of status){
|
||||
options.push({
|
||||
"value": option,
|
||||
"label": __(option),
|
||||
"description": ""
|
||||
})
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ from erpnext.controllers.print_settings import (
|
||||
from erpnext.controllers.sales_and_purchase_return import validate_return
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.stock.doctype.item.item import get_uom_conv_factor
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
_get_item_tax_template,
|
||||
@ -548,6 +549,15 @@ class AccountsController(TransactionBase):
|
||||
if ret.get("pricing_rules"):
|
||||
self.apply_pricing_rule_on_items(item, ret)
|
||||
self.set_pricing_rule_details(item, ret)
|
||||
else:
|
||||
# Transactions line item without item code
|
||||
|
||||
uom = item.get("uom")
|
||||
stock_uom = item.get("stock_uom")
|
||||
if bool(uom) != bool(stock_uom): # xor
|
||||
item.stock_uom = item.uom = uom or stock_uom
|
||||
|
||||
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
self.set_expense_account(for_validate)
|
||||
@ -1838,6 +1848,17 @@ class AccountsController(TransactionBase):
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
def check_conversion_rate(self):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
@ -2039,7 +2060,7 @@ def get_advance_journal_entries(
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||
'Journal Entry' as reference_type, t1.name as reference_name,
|
||||
t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t2.exchange_rate
|
||||
from
|
||||
@ -2094,7 +2115,7 @@ def get_advance_payment_entries(
|
||||
payment_entries_against_order = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as reference_type, t1.name as reference_name,
|
||||
'Payment Entry' as reference_type, t1.name as reference_name,
|
||||
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t1.posting_date,
|
||||
t1.{0} as currency, t1.{4} as exchange_rate
|
||||
@ -2114,7 +2135,7 @@ def get_advance_payment_entries(
|
||||
if include_unallocated:
|
||||
unallocated_payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select "Payment Entry" as reference_type, name as reference_name, posting_date,
|
||||
select 'Payment Entry' as reference_type, name as reference_name, posting_date,
|
||||
remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
@ -2419,7 +2440,7 @@ def update_bin_on_delete(row, doctype):
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data):
|
||||
def validate_and_delete_children(parent, data) -> bool:
|
||||
deleted_children = []
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
for item in parent.items:
|
||||
@ -2438,6 +2459,8 @@ def validate_and_delete_children(parent, data):
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
@ -2501,13 +2524,38 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
|
||||
def should_update_supplied_items(doc) -> bool:
|
||||
"""Subcontracted PO can allow following changes *after submit*:
|
||||
|
||||
1. Change rate of subcontracting - regardless of other changes.
|
||||
2. Change qty and/or add new items and/or remove items
|
||||
Exception: Transfer/Consumption is already made, qty change not allowed.
|
||||
"""
|
||||
|
||||
supplied_items_processed = any(
|
||||
item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items
|
||||
)
|
||||
|
||||
update_supplied_items = (
|
||||
any_qty_changed or items_added_or_removed or any_conversion_factor_changed
|
||||
)
|
||||
if update_supplied_items and supplied_items_processed:
|
||||
frappe.throw(_("Item qty can not be updated as raw materials are already processed."))
|
||||
|
||||
return update_supplied_items
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
validate_and_delete_children(parent, data)
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
items_added_or_removed |= _removed_items
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
@ -2518,6 +2566,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
items_added_or_removed = True
|
||||
check_doc_permissions(parent, "create")
|
||||
child_item = get_new_child_item(d)
|
||||
else:
|
||||
@ -2540,6 +2589,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
qty_unchanged = prev_qty == new_qty
|
||||
uom_unchanged = prev_uom == new_uom
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
date_unchanged = (
|
||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
||||
) # in case of delivery note etc
|
||||
@ -2553,6 +2603,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
continue
|
||||
|
||||
validate_quantity(child_item, d)
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
@ -2658,8 +2710,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
if parent.is_subcontracted:
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
parent.save()
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
|
@ -29,11 +29,11 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
or employee_name like %(txt)s)
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, employee_name) > 0 then locate(%(_txt)s, employee_name) else 99999 end),
|
||||
idx desc,
|
||||
name, employee_name
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
**{
|
||||
"fields": ", ".join(fields),
|
||||
"key": searchfield,
|
||||
@ -60,12 +60,12 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
or company_name like %(txt)s)
|
||||
{mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, lead_name), locate(%(_txt)s, lead_name), 99999),
|
||||
if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, lead_name) > 0 then locate(%(_txt)s, lead_name) else 99999 end),
|
||||
(case when locate(%(_txt)s, company_name) > 0 then locate(%(_txt)s, company_name) else 99999 end),
|
||||
idx desc,
|
||||
name, lead_name
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
**{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
@ -96,11 +96,11 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
and ({scond}) and disabled=0
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end),
|
||||
idx desc,
|
||||
name, customer_name
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
**{
|
||||
"fields": ", ".join(fields),
|
||||
"scond": searchfields,
|
||||
@ -130,14 +130,14 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
where docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or supplier_name like %(txt)s) and disabled=0
|
||||
and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
|
||||
and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date))
|
||||
{mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end),
|
||||
idx desc,
|
||||
name, supplier_name
|
||||
limit %(start)s, %(page_len)s """.format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
@ -167,7 +167,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
AND `{searchfield}` LIKE %(txt)s
|
||||
{mcond}
|
||||
ORDER BY idx DESC, name
|
||||
LIMIT %(offset)s, %(limit)s
|
||||
LIMIT %(limit)s offset %(offset)s
|
||||
""".format(
|
||||
account_type_condition=account_type_condition,
|
||||
searchfield=searchfield,
|
||||
@ -305,15 +305,15 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields}
|
||||
from tabBOM
|
||||
where tabBOM.docstatus=1
|
||||
and tabBOM.is_active=1
|
||||
and tabBOM.`{key}` like %(txt)s
|
||||
from `tabBOM`
|
||||
where `tabBOM`.docstatus=1
|
||||
and `tabBOM`.is_active=1
|
||||
and `tabBOM`.`{key}` like %(txt)s
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
idx desc, name
|
||||
limit %(start)s, %(page_len)s """.format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
fields=", ".join(fields),
|
||||
fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
|
||||
mcond=get_match_cond(doctype).replace("%", "%%"),
|
||||
@ -340,18 +340,18 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
fields = get_fields("Project", ["name", "project_name"])
|
||||
searchfields = frappe.get_meta("Project").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabProject`
|
||||
where
|
||||
`tabProject`.status not in ("Completed", "Cancelled")
|
||||
`tabProject`.status not in ('Completed', 'Cancelled')
|
||||
and {cond} {scond} {match_cond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
idx desc,
|
||||
(case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end),
|
||||
`tabProject`.idx desc,
|
||||
`tabProject`.name asc
|
||||
limit {start}, {page_len}""".format(
|
||||
limit {page_len} offset {start}""".format(
|
||||
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
|
||||
cond=cond,
|
||||
scond=searchfields,
|
||||
@ -374,7 +374,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
||||
from `tabDelivery Note`
|
||||
where `tabDelivery Note`.`%(key)s` like %(txt)s and
|
||||
`tabDelivery Note`.docstatus = 1
|
||||
and status not in ("Stopped", "Closed") %(fcond)s
|
||||
and status not in ('Stopped', 'Closed') %(fcond)s
|
||||
and (
|
||||
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
@ -383,7 +383,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
||||
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
||||
)
|
||||
)
|
||||
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s
|
||||
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(page_len)s offset %(start)s
|
||||
"""
|
||||
% {
|
||||
"fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]),
|
||||
@ -456,7 +456,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
{match_conditions}
|
||||
group by batch_no {having_clause}
|
||||
order by batch.expiry_date, sle.batch_no desc
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
search_columns=search_columns,
|
||||
cond=cond,
|
||||
match_conditions=get_match_cond(doctype),
|
||||
@ -483,7 +483,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
{match_conditions}
|
||||
|
||||
order by expiry_date, name desc
|
||||
limit %(start)s, %(page_len)s""".format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
cond,
|
||||
search_columns=search_columns,
|
||||
search_cond=search_cond,
|
||||
@ -654,7 +654,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
query = """select `tabWarehouse`.name,
|
||||
CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
|
||||
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
|
||||
from `tabWarehouse` left join `tabBin`
|
||||
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
|
||||
where
|
||||
@ -662,7 +662,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
{fcond} {mcond}
|
||||
order by ifnull(`tabBin`.actual_qty, 0) desc
|
||||
limit
|
||||
{start}, {page_len}
|
||||
{page_len} offset {start}
|
||||
""".format(
|
||||
bin_conditions=get_filters_cond(
|
||||
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
|
||||
@ -691,7 +691,7 @@ def get_doctype_wise_filters(filters):
|
||||
def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters):
|
||||
query = """select batch_id from `tabBatch`
|
||||
where disabled = 0
|
||||
and (expiry_date >= CURDATE() or expiry_date IS NULL)
|
||||
and (expiry_date >= CURRENT_DATE or expiry_date IS NULL)
|
||||
and name like {txt}""".format(
|
||||
txt=frappe.db.escape("%{0}%".format(txt))
|
||||
)
|
||||
|
@ -35,7 +35,8 @@ status_map = {
|
||||
["Draft", None],
|
||||
["Open", "eval:self.docstatus==1"],
|
||||
["Lost", "eval:self.status=='Lost'"],
|
||||
["Ordered", "has_sales_order"],
|
||||
["Partially Ordered", "is_partially_ordered"],
|
||||
["Ordered", "is_fully_ordered"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
],
|
||||
"Sales Order": [
|
||||
@ -351,9 +352,9 @@ class StatusUpdater(Document):
|
||||
for args in self.status_updater:
|
||||
# condition to include current record (if submit or no if cancel)
|
||||
if self.docstatus == 1:
|
||||
args["cond"] = ' or parent="%s"' % self.name.replace('"', '"')
|
||||
args["cond"] = " or parent='%s'" % self.name.replace('"', '"')
|
||||
else:
|
||||
args["cond"] = ' and parent!="%s"' % self.name.replace('"', '"')
|
||||
args["cond"] = " and parent!='%s'" % self.name.replace('"', '"')
|
||||
|
||||
self._update_children(args, update_modified)
|
||||
|
||||
@ -383,7 +384,7 @@ class StatusUpdater(Document):
|
||||
args["second_source_condition"] = frappe.db.sql(
|
||||
""" select ifnull((select sum(%(second_source_field)s)
|
||||
from `tab%(second_source_dt)s`
|
||||
where `%(second_join_field)s`="%(detail_id)s"
|
||||
where `%(second_join_field)s`='%(detail_id)s'
|
||||
and (`tab%(second_source_dt)s`.docstatus=1)
|
||||
%(second_source_extra_cond)s), 0) """
|
||||
% args
|
||||
@ -397,7 +398,7 @@ class StatusUpdater(Document):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
(select ifnull(sum(%(source_field)s), 0)
|
||||
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
|
||||
from `tab%(source_dt)s` where `%(join_field)s`='%(detail_id)s'
|
||||
and (docstatus=1 %(cond)s) %(extra_cond)s)
|
||||
"""
|
||||
% args
|
||||
@ -442,9 +443,9 @@ class StatusUpdater(Document):
|
||||
"""update `tab%(target_parent_dt)s`
|
||||
set %(target_parent_field)s = round(
|
||||
ifnull((select
|
||||
ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0)
|
||||
ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0)
|
||||
/ sum(abs(%(target_ref_field)s)) * 100
|
||||
from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||
from `tab%(target_dt)s` where parent='%(name)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||
%(update_modified)s
|
||||
where name='%(name)s'"""
|
||||
% args
|
||||
@ -454,9 +455,9 @@ class StatusUpdater(Document):
|
||||
if args.get("status_field"):
|
||||
frappe.db.sql(
|
||||
"""update `tab%(target_parent_dt)s`
|
||||
set %(status_field)s = if(%(target_parent_field)s<0.001,
|
||||
'Not %(keyword)s', if(%(target_parent_field)s>=99.999999,
|
||||
'Fully %(keyword)s', 'Partly %(keyword)s'))
|
||||
set %(status_field)s = (case when %(target_parent_field)s<0.001 then 'Not %(keyword)s'
|
||||
else case when %(target_parent_field)s>=99.999999 then 'Fully %(keyword)s'
|
||||
else 'Partly %(keyword)s' end end)
|
||||
where name='%(name)s'"""
|
||||
% args
|
||||
)
|
||||
|
@ -166,7 +166,7 @@ class StockController(AccountsController):
|
||||
"against": warehouse_account[sle.warehouse]["account"],
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(sle.stock_value_difference, precision),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
|
48
erpnext/crm/doctype/crm_note/crm_note.json
Normal file
48
erpnext/crm/doctype/crm_note/crm_note.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-06-04 15:49:23.416644",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"note",
|
||||
"added_by",
|
||||
"added_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 5,
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "added_by",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Added By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "added_on",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Added On"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-04 16:29:07.807252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Note",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
9
erpnext/crm/doctype/crm_note/crm_note.py
Normal file
9
erpnext/crm/doctype/crm_note/crm_note.py
Normal file
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMNote(Document):
|
||||
pass
|
@ -10,12 +10,10 @@
|
||||
"campaign_naming_by",
|
||||
"allow_lead_duplication_based_on_emails",
|
||||
"column_break_4",
|
||||
"create_event_on_next_contact_date",
|
||||
"auto_creation_of_contact",
|
||||
"opportunity_section",
|
||||
"close_opportunity_after_days",
|
||||
"column_break_9",
|
||||
"create_event_on_next_contact_date_opportunity",
|
||||
"quotation_section",
|
||||
"default_valid_till",
|
||||
"section_break_13",
|
||||
@ -55,12 +53,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Creation of Contact"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_event_on_next_contact_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Event on Next Contact Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity_section",
|
||||
"fieldtype": "Section Break",
|
||||
@ -73,12 +65,6 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Close Replied Opportunity After Days"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_event_on_next_contact_date_opportunity",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Event on Next Contact Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
@ -105,7 +91,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-20 12:51:38.894252",
|
||||
"modified": "2022-06-06 11:22:08.464253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
@ -143,5 +129,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -24,31 +24,39 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" }
|
||||
});
|
||||
|
||||
this.frm.set_query("contact_by", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" }
|
||||
});
|
||||
}
|
||||
|
||||
refresh () {
|
||||
var me = this;
|
||||
let doc = this.frm.doc;
|
||||
erpnext.toggle_naming_series();
|
||||
frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' }
|
||||
frappe.dynamic_link = {
|
||||
doc: doc,
|
||||
fieldname: 'name',
|
||||
doctype: 'Lead'
|
||||
};
|
||||
|
||||
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
||||
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), function() {
|
||||
me.frm.trigger("make_opportunity");
|
||||
}, __("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||
this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
|
||||
if (!doc.__onload.linked_prospects.length) {
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||
this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(this.frm);
|
||||
cur_frm.trigger('render_contact_day_html');
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(this.frm);
|
||||
}
|
||||
|
||||
this.frm.dashboard.links_area.hide();
|
||||
this.show_notes();
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
add_lead_to_prospect () {
|
||||
@ -74,7 +82,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('...Adding Lead to Prospect')
|
||||
freeze_message: __('Adding Lead to Prospect...')
|
||||
});
|
||||
}, __('Add Lead to Prospect'), __('Add'));
|
||||
}
|
||||
@ -86,13 +94,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
})
|
||||
}
|
||||
|
||||
make_opportunity () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
make_quotation () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
||||
@ -111,9 +112,10 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
prospect.fax = cur_frm.doc.fax;
|
||||
prospect.website = cur_frm.doc.website;
|
||||
prospect.prospect_owner = cur_frm.doc.lead_owner;
|
||||
prospect.notes = cur_frm.doc.notes;
|
||||
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
|
||||
lead_prospect_row.lead = cur_frm.doc.name;
|
||||
let leads_row = frappe.model.add_child(prospect, 'leads');
|
||||
leads_row.lead = cur_frm.doc.name;
|
||||
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
});
|
||||
@ -125,26 +127,109 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
}
|
||||
|
||||
contact_date () {
|
||||
if (this.frm.doc.contact_date) {
|
||||
let d = moment(this.frm.doc.contact_date);
|
||||
d.add(1, "day");
|
||||
this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
|
||||
}
|
||||
show_notes() {
|
||||
if (this.frm.doc.docstatus == 1) return;
|
||||
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: this.frm,
|
||||
notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
}
|
||||
|
||||
render_contact_day_html() {
|
||||
if (cur_frm.doc.contact_date) {
|
||||
let contact_date = frappe.datetime.obj_to_str(cur_frm.doc.contact_date);
|
||||
let diff_days = frappe.datetime.get_day_diff(contact_date, frappe.datetime.get_today());
|
||||
let color = diff_days > 0 ? "orange" : "green";
|
||||
let message = diff_days > 0 ? __("Next Contact Date") : __("Last Contact Date");
|
||||
let html = `<div class="col-xs-12">
|
||||
<span class="indicator whitespace-nowrap ${color}"><span> ${message} : ${frappe.datetime.global_date_format(contact_date)}</span></span>
|
||||
</div>` ;
|
||||
cur_frm.dashboard.set_headline_alert(html);
|
||||
}
|
||||
show_activities() {
|
||||
if (this.frm.doc.docstatus == 1) return;
|
||||
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: this.frm,
|
||||
open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(this.frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
||||
|
||||
frappe.ui.form.on("Lead", {
|
||||
make_opportunity: async function(frm) {
|
||||
let existing_prospect = (await frappe.db.get_value("Prospect Lead",
|
||||
{
|
||||
"lead": frm.doc.name
|
||||
},
|
||||
"name", null, "Prospect"
|
||||
)).message.name;
|
||||
|
||||
if (!existing_prospect) {
|
||||
var fields = [
|
||||
{
|
||||
"label": "Create Prospect",
|
||||
"fieldname": "create_prospect",
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"label": "Prospect Name",
|
||||
"fieldname": "prospect_name",
|
||||
"fieldtype": "Data",
|
||||
"default": frm.doc.company_name,
|
||||
"depends_on": "create_prospect"
|
||||
}
|
||||
];
|
||||
}
|
||||
let existing_contact = (await frappe.db.get_value("Contact",
|
||||
{
|
||||
"first_name": frm.doc.first_name || frm.doc.lead_name,
|
||||
"last_name": frm.doc.last_name
|
||||
},
|
||||
"name"
|
||||
)).message.name;
|
||||
|
||||
if (!existing_contact) {
|
||||
fields.push(
|
||||
{
|
||||
"label": "Create Contact",
|
||||
"fieldname": "create_contact",
|
||||
"fieldtype": "Check",
|
||||
"default": "1"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Create Opportunity'),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'create_prospect_and_contact',
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
data: data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Create')
|
||||
});
|
||||
d.show();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -3,78 +3,80 @@
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-04-10 11:45:37",
|
||||
"creation": "2022-02-08 13:14:41.083327",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"lead_details",
|
||||
"naming_series",
|
||||
"salutation",
|
||||
"first_name",
|
||||
"middle_name",
|
||||
"last_name",
|
||||
"column_break_1",
|
||||
"lead_name",
|
||||
"col_break123",
|
||||
"status",
|
||||
"company_name",
|
||||
"designation",
|
||||
"job_title",
|
||||
"gender",
|
||||
"contact_details_section",
|
||||
"source",
|
||||
"col_break123",
|
||||
"lead_owner",
|
||||
"status",
|
||||
"customer",
|
||||
"type",
|
||||
"request_type",
|
||||
"contact_info_tab",
|
||||
"email_id",
|
||||
"website",
|
||||
"column_break_20",
|
||||
"mobile_no",
|
||||
"whatsapp_no",
|
||||
"column_break_16",
|
||||
"phone",
|
||||
"phone_ext",
|
||||
"additional_information_section",
|
||||
"organization_section",
|
||||
"company_name",
|
||||
"no_of_employees",
|
||||
"column_break_28",
|
||||
"annual_revenue",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"column_break_22",
|
||||
"column_break_31",
|
||||
"territory",
|
||||
"fax",
|
||||
"website",
|
||||
"type",
|
||||
"request_type",
|
||||
"address_section",
|
||||
"address_html",
|
||||
"column_break_38",
|
||||
"city",
|
||||
"pincode",
|
||||
"county",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"state",
|
||||
"country",
|
||||
"section_break_12",
|
||||
"lead_owner",
|
||||
"ends_on",
|
||||
"column_break_14",
|
||||
"contact_by",
|
||||
"contact_date",
|
||||
"lead_source_details_section",
|
||||
"company",
|
||||
"territory",
|
||||
"language",
|
||||
"column_break_50",
|
||||
"source",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"qualification_tab",
|
||||
"qualification_status",
|
||||
"column_break_64",
|
||||
"qualified_by",
|
||||
"qualified_on",
|
||||
"other_info_tab",
|
||||
"campaign_name",
|
||||
"company",
|
||||
"column_break_22",
|
||||
"language",
|
||||
"image",
|
||||
"title",
|
||||
"column_break_50",
|
||||
"disabled",
|
||||
"unsubscribed",
|
||||
"blog_subscriber",
|
||||
"notes_section",
|
||||
"notes",
|
||||
"other_information_section",
|
||||
"customer",
|
||||
"image",
|
||||
"title"
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_tab",
|
||||
"notes_html",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lead_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Details",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -86,6 +88,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -108,7 +111,7 @@
|
||||
{
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email Address",
|
||||
"label": "Email",
|
||||
"oldfieldname": "email_id",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Email",
|
||||
@ -189,50 +192,9 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldname": "contact_info_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Next Contact By",
|
||||
"oldfieldname": "contact_by",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "contact_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Next Contact Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "contact_date",
|
||||
"oldfieldtype": "Date",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "ends_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Ends On",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "notes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Notes"
|
||||
"label": "Contact Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
@ -240,34 +202,6 @@
|
||||
"label": "Address HTML",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City/Town",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "county",
|
||||
"fieldtype": "Data",
|
||||
"label": "County"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type",
|
||||
"options": "Country"
|
||||
},
|
||||
{
|
||||
"fieldname": "pincode",
|
||||
"fieldtype": "Data",
|
||||
"label": "Postal Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break"
|
||||
@ -289,7 +223,7 @@
|
||||
{
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mobile No.",
|
||||
"label": "Mobile No",
|
||||
"oldfieldname": "mobile_no",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Phone"
|
||||
@ -347,8 +281,7 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Website",
|
||||
"oldfieldname": "website",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "URL"
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
@ -380,14 +313,6 @@
|
||||
"label": "Title",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Designation",
|
||||
"options": "Designation"
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
@ -410,12 +335,6 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Last Name"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "additional_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
@ -428,35 +347,13 @@
|
||||
{
|
||||
"fieldname": "whatsapp_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "WhatsApp No.",
|
||||
"label": "WhatsApp",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "lead_source_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Source Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_50",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "other_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Other Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
@ -465,17 +362,156 @@
|
||||
"fieldname": "phone_ext",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Ext."
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "qualification_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Qualification"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "other_info_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualified_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Qualified By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualified_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Qualified on"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualification_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Qualification Status",
|
||||
"options": "Unqualified\nIn Process\nQualified"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_64",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_20",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Job Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_28",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-08-04 00:24:57.208590",
|
||||
"modified": "2022-06-27 21:56:17.392756",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -535,6 +571,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"subject_field": "title",
|
||||
"title_field": "title"
|
||||
}
|
@ -1,27 +1,19 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import (
|
||||
comma_and,
|
||||
cstr,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
has_gravatar,
|
||||
nowdate,
|
||||
validate_email_address,
|
||||
)
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
|
||||
|
||||
class Lead(SellingController):
|
||||
class Lead(SellingController, CRMNote):
|
||||
def get_feed(self):
|
||||
return "{0}: {1}".format(_(self.status), self.lead_name)
|
||||
|
||||
@ -29,6 +21,7 @@ class Lead(SellingController):
|
||||
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
|
||||
self.get("__onload").is_customer = customer
|
||||
load_address_and_contact(self)
|
||||
self.set_onload("linked_prospects", self.get_linked_prospects())
|
||||
|
||||
def validate(self):
|
||||
self.set_full_name()
|
||||
@ -37,79 +30,42 @@ class Lead(SellingController):
|
||||
self.set_status()
|
||||
self.check_email_id_is_unique()
|
||||
self.validate_email_id()
|
||||
self.validate_contact_date()
|
||||
self.set_prev()
|
||||
|
||||
def before_insert(self):
|
||||
self.contact_doc = None
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
def after_insert(self):
|
||||
self.link_to_contact()
|
||||
|
||||
def on_update(self):
|
||||
self.update_prospect()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
self.remove_link_from_prospect()
|
||||
|
||||
def set_full_name(self):
|
||||
if self.first_name:
|
||||
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.email_id:
|
||||
if not self.flags.ignore_email_validation:
|
||||
validate_email_address(self.email_id, throw=True)
|
||||
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead"))
|
||||
|
||||
if self.email_id == self.contact_by:
|
||||
frappe.throw(_("Next Contact By cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def validate_contact_date(self):
|
||||
if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
|
||||
frappe.throw(_("Next Contact Date cannot be in the past"))
|
||||
|
||||
if self.ends_on and self.contact_date and (getdate(self.ends_on) < getdate(self.contact_date)):
|
||||
frappe.throw(_("Ends On date cannot be before Next Contact Date."))
|
||||
|
||||
def on_update(self):
|
||||
self.add_calendar_event()
|
||||
self.update_prospects()
|
||||
|
||||
def set_prev(self):
|
||||
if self.is_new():
|
||||
self._prev = frappe._dict({"contact_date": None, "ends_on": None, "contact_by": None})
|
||||
else:
|
||||
self._prev = frappe.db.get_value(
|
||||
"Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1
|
||||
self.lead_name = " ".join(
|
||||
filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name])
|
||||
)
|
||||
|
||||
def before_insert(self):
|
||||
self.contact_doc = self.create_contact()
|
||||
def set_lead_name(self):
|
||||
if not self.lead_name:
|
||||
# Check for leads being created through data import
|
||||
if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
|
||||
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
|
||||
elif self.company_name:
|
||||
self.lead_name = self.company_name
|
||||
else:
|
||||
self.lead_name = self.email_id.split("@")[0]
|
||||
|
||||
def after_insert(self):
|
||||
self.update_links()
|
||||
|
||||
def update_links(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
self.contact_doc.append(
|
||||
"links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
|
||||
)
|
||||
self.contact_doc.save()
|
||||
|
||||
def add_calendar_event(self, opts=None, force=False):
|
||||
if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date"):
|
||||
super(Lead, self).add_calendar_event(
|
||||
{
|
||||
"owner": self.lead_owner,
|
||||
"starts_on": self.contact_date,
|
||||
"ends_on": self.ends_on or "",
|
||||
"subject": ("Contact " + cstr(self.lead_name)),
|
||||
"description": ("Contact " + cstr(self.lead_name))
|
||||
+ (self.contact_by and (". By : " + cstr(self.contact_by)) or ""),
|
||||
},
|
||||
force,
|
||||
)
|
||||
|
||||
def update_prospects(self):
|
||||
prospects = frappe.get_all("Prospect Lead", filters={"lead": self.name}, fields=["parent"])
|
||||
for row in prospects:
|
||||
prospect = frappe.get_doc("Prospect", row.parent)
|
||||
prospect.save(ignore_permissions=True)
|
||||
def set_title(self):
|
||||
self.title = self.company_name or self.lead_name
|
||||
|
||||
def check_email_id_is_unique(self):
|
||||
if self.email_id:
|
||||
@ -124,15 +80,47 @@ class Lead(SellingController):
|
||||
|
||||
if duplicate_leads:
|
||||
frappe.throw(
|
||||
_("Email Address must be unique, already exists for {0}").format(comma_and(duplicate_leads)),
|
||||
_("Email Address must be unique, it is already used in {0}").format(
|
||||
comma_and(duplicate_leads)
|
||||
),
|
||||
frappe.DuplicateEntryError,
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
def validate_email_id(self):
|
||||
if self.email_id:
|
||||
if not self.flags.ignore_email_validation:
|
||||
validate_email_address(self.email_id, throw=True)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
self.delete_events()
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def link_to_contact(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
self.contact_doc.append(
|
||||
"links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
|
||||
)
|
||||
self.contact_doc.save()
|
||||
|
||||
def update_prospect(self):
|
||||
lead_row_name = frappe.db.get_value(
|
||||
"Prospect Lead", filters={"lead": self.name}, fieldname="name"
|
||||
)
|
||||
if lead_row_name:
|
||||
lead_row = frappe.get_doc("Prospect Lead", lead_row_name)
|
||||
lead_row.update(
|
||||
{
|
||||
"lead_name": self.lead_name,
|
||||
"email": self.email_id,
|
||||
"mobile_no": self.mobile_no,
|
||||
"lead_owner": self.lead_owner,
|
||||
"status": self.status,
|
||||
}
|
||||
)
|
||||
lead_row.db_update()
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
@ -155,6 +143,30 @@ class Lead(SellingController):
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def remove_link_from_prospect(self):
|
||||
prospects = self.get_linked_prospects()
|
||||
|
||||
for d in prospects:
|
||||
prospect = frappe.get_doc("Prospect", d.parent)
|
||||
if len(prospect.get("leads")) == 1:
|
||||
prospect.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in prospect.get("leads"):
|
||||
if d.lead == self.name:
|
||||
to_remove = d
|
||||
|
||||
if to_remove:
|
||||
prospect.remove(to_remove)
|
||||
prospect.save(ignore_permissions=True)
|
||||
|
||||
def get_linked_prospects(self):
|
||||
return frappe.get_all(
|
||||
"Prospect Lead",
|
||||
filters={"lead": self.name},
|
||||
fields=["parent"],
|
||||
)
|
||||
|
||||
def has_customer(self):
|
||||
return frappe.db.get_value("Customer", {"lead_name": self.name})
|
||||
|
||||
@ -171,50 +183,78 @@ class Lead(SellingController):
|
||||
"Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"}
|
||||
)
|
||||
|
||||
def set_lead_name(self):
|
||||
if not self.lead_name:
|
||||
# Check for leads being created through data import
|
||||
if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
|
||||
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
|
||||
elif self.company_name:
|
||||
self.lead_name = self.company_name
|
||||
else:
|
||||
self.lead_name = self.email_id.split("@")[0]
|
||||
@frappe.whitelist()
|
||||
def create_prospect_and_contact(self, data):
|
||||
data = frappe._dict(data)
|
||||
if data.create_contact:
|
||||
self.create_contact()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.company_name or self.lead_name
|
||||
if data.create_prospect:
|
||||
self.create_prospect(data.prospect_name)
|
||||
|
||||
def create_contact(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
if not self.lead_name:
|
||||
self.set_full_name()
|
||||
self.set_lead_name()
|
||||
if not self.lead_name:
|
||||
self.set_full_name()
|
||||
self.set_lead_name()
|
||||
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update(
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update(
|
||||
{
|
||||
"first_name": self.first_name or self.lead_name,
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"job_title": self.job_title,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
)
|
||||
|
||||
if self.email_id:
|
||||
contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
|
||||
|
||||
if self.phone:
|
||||
contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
|
||||
|
||||
if self.mobile_no:
|
||||
contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.reload() # load changes by hooks on contact
|
||||
|
||||
return contact
|
||||
|
||||
def create_prospect(self, company_name):
|
||||
try:
|
||||
prospect = frappe.new_doc("Prospect")
|
||||
|
||||
prospect.company_name = company_name or self.company_name
|
||||
prospect.no_of_employees = self.no_of_employees
|
||||
prospect.industry = self.industry
|
||||
prospect.market_segment = self.market_segment
|
||||
prospect.annual_revenue = self.annual_revenue
|
||||
prospect.territory = self.territory
|
||||
prospect.fax = self.fax
|
||||
prospect.website = self.website
|
||||
prospect.prospect_owner = self.lead_owner
|
||||
prospect.company = self.company
|
||||
prospect.notes = self.notes
|
||||
|
||||
prospect.append(
|
||||
"leads",
|
||||
{
|
||||
"first_name": self.first_name or self.lead_name,
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"designation": self.designation,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
"lead": self.name,
|
||||
"lead_name": self.lead_name,
|
||||
"email": self.email_id,
|
||||
"mobile_no": self.mobile_no,
|
||||
"lead_owner": self.lead_owner,
|
||||
"status": self.status,
|
||||
},
|
||||
)
|
||||
|
||||
if self.email_id:
|
||||
contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
|
||||
|
||||
if self.phone:
|
||||
contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
|
||||
|
||||
if self.mobile_no:
|
||||
contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.reload() # load changes by hooks on contact
|
||||
|
||||
return contact
|
||||
prospect.flags.ignore_permissions = True
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
except frappe.DuplicateEntryError:
|
||||
frappe.throw(_("Prospect {0} already exists").format(company_name or self.company_name))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -274,6 +314,8 @@ def make_opportunity(source_name, target_doc=None):
|
||||
"company_name": "customer_name",
|
||||
"email_id": "contact_email",
|
||||
"mobile_no": "contact_mobile",
|
||||
"lead_owner": "opportunity_owner",
|
||||
"notes": "notes",
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -422,21 +464,25 @@ def get_lead_with_phone_number(number):
|
||||
return lead
|
||||
|
||||
|
||||
def daily_open_lead():
|
||||
leads = frappe.get_all("Lead", filters=[["contact_date", "Between", [nowdate(), nowdate()]]])
|
||||
for lead in leads:
|
||||
frappe.db.set_value("Lead", lead.name, "status", "Open")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_lead_to_prospect(lead, prospect):
|
||||
prospect = frappe.get_doc("Prospect", prospect)
|
||||
prospect.append("prospect_lead", {"lead": lead})
|
||||
prospect.append("leads", {"lead": lead})
|
||||
prospect.save(ignore_permissions=True)
|
||||
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
)
|
||||
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Lead", lead, prospect)
|
||||
link_communications("Lead", lead, prospect)
|
||||
link_open_events("Lead", lead, prospect)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Lead {0} has been added to prospect {1}.").format(
|
||||
frappe.bold(lead), frappe.bold(prospect.name)
|
||||
),
|
||||
title=_("Lead Added"),
|
||||
title=_("Lead -> Prospect"),
|
||||
indicator="green",
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ frappe.listview_settings['Lead'] = {
|
||||
prospect.prospect_owner = r.lead_owner;
|
||||
|
||||
leads.forEach(function(lead) {
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'leads');
|
||||
lead_prospect_row.lead = lead.name;
|
||||
});
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
|
@ -5,7 +5,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils import random_string, today
|
||||
|
||||
from erpnext.crm.doctype.lead.lead import make_opportunity
|
||||
from erpnext.crm.utils import get_linked_prospect
|
||||
|
||||
test_records = frappe.get_test_records("Lead")
|
||||
|
||||
@ -83,6 +86,105 @@ class TestLead(unittest.TestCase):
|
||||
self.assertEqual(frappe.db.exists("Lead", lead_doc.name), None)
|
||||
self.assertEqual(len(address_1.get("links")), 1)
|
||||
|
||||
def test_prospect_creation_from_lead(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
event = create_event("Meeting 1", today(), "Lead", lead.name)
|
||||
|
||||
lead.create_prospect(lead.company_name)
|
||||
|
||||
prospect = get_linked_prospect("Lead", lead.name)
|
||||
self.assertEqual(prospect, "Prospect Company")
|
||||
|
||||
event.reload()
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, prospect)
|
||||
|
||||
def test_opportunity_from_lead(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabOpportunity` where party_name='Rahul Tripathi'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
lead.add_note("test note")
|
||||
event = create_event("Meeting 1", today(), "Lead", lead.name)
|
||||
create_todo("followup", "Lead", lead.name)
|
||||
|
||||
opportunity = make_opportunity(lead.name)
|
||||
opportunity.save()
|
||||
|
||||
self.assertEqual(opportunity.get("party_name"), lead.name)
|
||||
self.assertEqual(opportunity.notes[0].note, "test note")
|
||||
|
||||
event.reload()
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Opportunity")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, opportunity.name)
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.get_value(
|
||||
"ToDo", {"reference_type": "Opportunity", "reference_name": opportunity.name}
|
||||
)
|
||||
)
|
||||
|
||||
def test_copy_events_from_lead_to_prospect(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
lead.create_prospect(lead.company_name)
|
||||
prospect = get_linked_prospect("Lead", lead.name)
|
||||
|
||||
event = create_event("Meeting", today(), "Lead", lead.name)
|
||||
|
||||
self.assertEqual(len(event.event_participants), 2)
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, prospect)
|
||||
|
||||
|
||||
def create_event(subject, starts_on, reference_type, reference_name):
|
||||
event = frappe.new_doc("Event")
|
||||
event.subject = subject
|
||||
event.starts_on = starts_on
|
||||
event.event_type = "Private"
|
||||
event.all_day = 1
|
||||
event.owner = "Administrator"
|
||||
event.append(
|
||||
"event_participants", {"reference_doctype": reference_type, "reference_docname": reference_name}
|
||||
)
|
||||
event.reference_type = reference_type
|
||||
event.reference_name = reference_name
|
||||
event.insert()
|
||||
return event
|
||||
|
||||
|
||||
def create_todo(description, reference_type, reference_name):
|
||||
todo = frappe.new_doc("ToDo")
|
||||
todo.description = description
|
||||
todo.owner = "Administrator"
|
||||
todo.reference_type = reference_type
|
||||
todo.reference_name = reference_name
|
||||
todo.insert()
|
||||
return todo
|
||||
|
||||
|
||||
def make_lead(**args):
|
||||
args = frappe._dict(args)
|
||||
@ -93,6 +195,7 @@ def make_lead(**args):
|
||||
"first_name": args.first_name or "_Test",
|
||||
"last_name": args.last_name or "Lead",
|
||||
"email_id": args.email_id or "new_lead_{}@example.com".format(random_string(5)),
|
||||
"company_name": args.company_name or "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
@ -32,13 +32,6 @@ frappe.ui.form.on("Opportunity", {
|
||||
}
|
||||
},
|
||||
|
||||
contact_date: function(frm) {
|
||||
if(frm.doc.contact_date < frappe.datetime.now_datetime()){
|
||||
frm.set_value("contact_date", "");
|
||||
frappe.throw(__("Next follow up date should be greater than now."))
|
||||
}
|
||||
},
|
||||
|
||||
onload_post_render: function(frm) {
|
||||
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
|
||||
},
|
||||
@ -130,6 +123,13 @@ frappe.ui.form.on("Opportunity", {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
// frm.trigger('render_contact_day_html');
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
},
|
||||
|
||||
set_contact_link: function(frm) {
|
||||
@ -227,8 +227,7 @@ frappe.ui.form.on("Opportunity", {
|
||||
'total': flt(total),
|
||||
'base_total': flt(base_total)
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
frappe.ui.form.on("Opportunity Item", {
|
||||
calculate: function(frm, cdt, cdn) {
|
||||
@ -264,13 +263,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
this.frm.trigger('currency');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.show_notes();
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
if(this.frm.fields_dict.contact_by.df.options.match(/^User/)) {
|
||||
this.frm.set_query("contact_by", erpnext.queries.user);
|
||||
}
|
||||
|
||||
me.frm.set_query('customer_address', erpnext.queries.address_query);
|
||||
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
@ -287,6 +287,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
}
|
||||
else if (me.frm.doc.opportunity_from == "Customer") {
|
||||
me.frm.set_query('party_name', erpnext.queries['customer']);
|
||||
} else if (me.frm.doc.opportunity_from == "Prospect") {
|
||||
me.frm.set_query('party_name', function() {
|
||||
return {
|
||||
filters: {
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,6 +311,24 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
show_notes() {
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: this.frm,
|
||||
notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
}
|
||||
|
||||
show_activities() {
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: this.frm,
|
||||
open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(this.frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "naming_series:",
|
||||
@ -11,68 +12,87 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_section",
|
||||
"naming_series",
|
||||
"opportunity_from",
|
||||
"party_name",
|
||||
"customer_name",
|
||||
"source",
|
||||
"column_break0",
|
||||
"title",
|
||||
"opportunity_type",
|
||||
"status",
|
||||
"converted_by",
|
||||
"column_break0",
|
||||
"opportunity_type",
|
||||
"source",
|
||||
"opportunity_owner",
|
||||
"column_break_10",
|
||||
"sales_stage",
|
||||
"first_response_time",
|
||||
"expected_closing",
|
||||
"next_contact",
|
||||
"contact_by",
|
||||
"contact_date",
|
||||
"column_break2",
|
||||
"to_discuss",
|
||||
"probability",
|
||||
"organization_details_section",
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"customer_group",
|
||||
"column_break_23",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"website",
|
||||
"column_break_31",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"territory",
|
||||
"section_break_14",
|
||||
"currency",
|
||||
"column_break_36",
|
||||
"conversion_rate",
|
||||
"base_opportunity_amount",
|
||||
"with_items",
|
||||
"column_break_17",
|
||||
"probability",
|
||||
"opportunity_amount",
|
||||
"base_opportunity_amount",
|
||||
"more_info",
|
||||
"company",
|
||||
"campaign",
|
||||
"transaction_date",
|
||||
"column_break1",
|
||||
"language",
|
||||
"amended_from",
|
||||
"title",
|
||||
"first_response_time",
|
||||
"lost_detail_section",
|
||||
"lost_reasons",
|
||||
"order_lost_reason",
|
||||
"column_break_56",
|
||||
"competitors",
|
||||
"contact_info",
|
||||
"primary_contact_section",
|
||||
"contact_person",
|
||||
"job_title",
|
||||
"column_break_54",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"column_break_22",
|
||||
"whatsapp",
|
||||
"phone",
|
||||
"phone_ext",
|
||||
"address_contact_section",
|
||||
"address_html",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break3",
|
||||
"contact_html",
|
||||
"contact_display",
|
||||
"items_section",
|
||||
"items",
|
||||
"section_break_32",
|
||||
"base_total",
|
||||
"column_break_33",
|
||||
"total",
|
||||
"contact_info",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"territory",
|
||||
"customer_group",
|
||||
"column_break3",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"more_info",
|
||||
"company",
|
||||
"campaign",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"language",
|
||||
"amended_from",
|
||||
"lost_detail_section",
|
||||
"lost_reasons",
|
||||
"order_lost_reason",
|
||||
"column_break_56",
|
||||
"competitors"
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_tab",
|
||||
"notes_html",
|
||||
"notes",
|
||||
"dashboard_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "from_section",
|
||||
"fieldtype": "Section Break",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -113,8 +133,9 @@
|
||||
"bold": 1,
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_global_search": 1,
|
||||
"label": "Customer / Lead Name",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -166,48 +187,10 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Closing Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "contact_by",
|
||||
"fieldname": "next_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Next Contact By",
|
||||
"oldfieldname": "contact_by",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": "75px"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Next Contact Date",
|
||||
"oldfieldname": "contact_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_discuss",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "To Discuss",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "to_discuss",
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sales"
|
||||
"label": "Opportunity Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
@ -221,12 +204,6 @@
|
||||
"label": "Opportunity Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "with_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "With Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
@ -245,9 +222,8 @@
|
||||
"label": "Probability (%)"
|
||||
},
|
||||
{
|
||||
"depends_on": "with_items",
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Items",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
@ -262,18 +238,16 @@
|
||||
"options": "Opportunity Item"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "next_contact_by",
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Info",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contacts",
|
||||
"options": "fa fa-bullhorn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "customer_address",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Customer / Lead Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
@ -327,19 +301,16 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact Mobile No",
|
||||
"read_only": 1
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Mobile",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@ -416,12 +387,6 @@
|
||||
"options": "Opportunity Lost Reason Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "converted_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Converted By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "first_response_time",
|
||||
@ -474,6 +439,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status===\"Lost\"",
|
||||
"fieldname": "lost_detail_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lost Reasons"
|
||||
@ -488,12 +454,179 @@
|
||||
"label": "Competitors",
|
||||
"options": "Competitor Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
"label": "No of Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
},
|
||||
{
|
||||
"fieldname": "industry",
|
||||
"fieldtype": "Link",
|
||||
"label": "Industry",
|
||||
"options": "Industry Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Market Segment",
|
||||
"options": "Market Segment"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity_owner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Opportunity Owner",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "whatsapp",
|
||||
"fieldtype": "Data",
|
||||
"label": "WhatsApp",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_ext",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Ext."
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Primary Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_54",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "dashboard_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Job Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Contact HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2022-01-29 19:32:26.382896",
|
||||
"modified": "2022-06-27 18:44:32.858696",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
@ -6,52 +6,54 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, get_fullname
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.utils import flt, get_fullname
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.crm.utils import (
|
||||
CRMNote,
|
||||
copy_comments,
|
||||
link_communications,
|
||||
link_open_events,
|
||||
link_open_tasks,
|
||||
)
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
|
||||
class Opportunity(TransactionBase):
|
||||
class Opportunity(TransactionBase, CRMNote):
|
||||
def onload(self):
|
||||
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
||||
load_address_and_contact(ref_doc)
|
||||
self.set("__onload", ref_doc.get("__onload"))
|
||||
|
||||
def after_insert(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.get_doc("Lead", self.party_name).set_status(update=True)
|
||||
self.disable_lead()
|
||||
|
||||
if self.opportunity_from in ["Lead", "Prospect"]:
|
||||
link_open_tasks(self.opportunity_from, self.party_name, self)
|
||||
link_open_events(self.opportunity_from, self.party_name, self)
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
copy_comments(self.opportunity_from, self.party_name, self)
|
||||
add_link_in_communication(self.opportunity_from, self.party_name, self)
|
||||
link_communications(self.opportunity_from, self.party_name, self)
|
||||
|
||||
def validate(self):
|
||||
self._prev = frappe._dict(
|
||||
{
|
||||
"contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date")
|
||||
if (not cint(self.get("__islocal")))
|
||||
else None,
|
||||
"contact_by": frappe.db.get_value("Opportunity", self.name, "contact_by")
|
||||
if (not cint(self.get("__islocal")))
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
self.make_new_lead_if_required()
|
||||
self.validate_item_details()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_cust_name()
|
||||
self.map_fields()
|
||||
self.set_exchange_rate()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.customer_name
|
||||
|
||||
if not self.with_items:
|
||||
self.items = []
|
||||
|
||||
else:
|
||||
self.calculate_totals()
|
||||
self.calculate_totals()
|
||||
self.update_prospect()
|
||||
|
||||
def map_fields(self):
|
||||
for field in self.meta.get_valid_columns():
|
||||
@ -62,18 +64,65 @@ class Opportunity(TransactionBase):
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def set_exchange_rate(self):
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.currency == company_currency:
|
||||
self.conversion_rate = 1.0
|
||||
return
|
||||
|
||||
if not self.conversion_rate or self.conversion_rate == 1.0:
|
||||
self.conversion_rate = get_exchange_rate(self.currency, company_currency, self.transaction_date)
|
||||
|
||||
def calculate_totals(self):
|
||||
total = base_total = 0
|
||||
for item in self.get("items"):
|
||||
item.amount = flt(item.rate) * flt(item.qty)
|
||||
item.base_rate = flt(self.conversion_rate * item.rate)
|
||||
item.base_amount = flt(self.conversion_rate * item.amount)
|
||||
item.base_rate = flt(self.conversion_rate) * flt(item.rate)
|
||||
item.base_amount = flt(self.conversion_rate) * flt(item.amount)
|
||||
total += item.amount
|
||||
base_total += item.base_amount
|
||||
|
||||
self.total = flt(total)
|
||||
self.base_total = flt(base_total)
|
||||
|
||||
def update_prospect(self):
|
||||
prospect_name = None
|
||||
if self.opportunity_from == "Prospect" and self.party_name:
|
||||
prospect_name = self.party_name
|
||||
elif self.opportunity_from == "Lead":
|
||||
prospect_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
|
||||
|
||||
if prospect_name:
|
||||
prospect = frappe.get_doc("Prospect", prospect_name)
|
||||
|
||||
opportunity_values = {
|
||||
"opportunity": self.name,
|
||||
"amount": self.opportunity_amount,
|
||||
"stage": self.sales_stage,
|
||||
"deal_owner": self.opportunity_owner,
|
||||
"probability": self.probability,
|
||||
"expected_closing": self.expected_closing,
|
||||
"currency": self.currency,
|
||||
"contact_person": self.contact_person,
|
||||
}
|
||||
|
||||
opportunity_already_added = False
|
||||
for d in prospect.get("opportunities", []):
|
||||
if d.opportunity == self.name:
|
||||
opportunity_already_added = True
|
||||
d.update(opportunity_values)
|
||||
d.db_update()
|
||||
|
||||
if not opportunity_already_added:
|
||||
prospect.append("opportunities", opportunity_values)
|
||||
prospect.flags.ignore_permissions = True
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
|
||||
def disable_lead(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
|
||||
|
||||
def make_new_lead_if_required(self):
|
||||
"""Set lead against new opportunity"""
|
||||
if (not self.get("party_name")) and self.contact_email:
|
||||
@ -143,11 +192,8 @@ class Opportunity(TransactionBase):
|
||||
else:
|
||||
frappe.throw(_("Cannot declare as lost, because Quotation has been made."))
|
||||
|
||||
def on_trash(self):
|
||||
self.delete_events()
|
||||
|
||||
def has_active_quotation(self):
|
||||
if not self.with_items:
|
||||
if not self.get("items", []):
|
||||
return frappe.get_all(
|
||||
"Quotation",
|
||||
{"opportunity": self.name, "status": ("not in", ["Lost", "Closed"]), "docstatus": 1},
|
||||
@ -164,7 +210,7 @@ class Opportunity(TransactionBase):
|
||||
)
|
||||
|
||||
def has_ordered_quotation(self):
|
||||
if not self.with_items:
|
||||
if not self.get("items", []):
|
||||
return frappe.get_all(
|
||||
"Quotation", {"opportunity": self.name, "status": "Ordered", "docstatus": 1}, "name"
|
||||
)
|
||||
@ -194,43 +240,20 @@ class Opportunity(TransactionBase):
|
||||
return True
|
||||
|
||||
def validate_cust_name(self):
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
lead_name, company_name = frappe.db.get_value(
|
||||
"Lead", self.party_name, ["lead_name", "company_name"]
|
||||
)
|
||||
self.customer_name = company_name or lead_name
|
||||
if self.party_name:
|
||||
if self.opportunity_from == "Customer":
|
||||
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
|
||||
elif self.opportunity_from == "Lead":
|
||||
customer_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
|
||||
if not customer_name:
|
||||
lead_name, company_name = frappe.db.get_value(
|
||||
"Lead", self.party_name, ["lead_name", "company_name"]
|
||||
)
|
||||
customer_name = company_name or lead_name
|
||||
|
||||
def on_update(self):
|
||||
self.add_calendar_event()
|
||||
|
||||
def add_calendar_event(self, opts=None, force=False):
|
||||
if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date_opportunity"):
|
||||
if not opts:
|
||||
opts = frappe._dict()
|
||||
|
||||
opts.description = ""
|
||||
opts.contact_date = self.contact_date
|
||||
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
if self.contact_person:
|
||||
opts.description = f"Contact {self.contact_person}"
|
||||
else:
|
||||
opts.description = f"Contact customer {self.party_name}"
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
if self.contact_display:
|
||||
opts.description = f"Contact {self.contact_display}"
|
||||
else:
|
||||
opts.description = f"Contact lead {self.party_name}"
|
||||
|
||||
opts.subject = opts.description
|
||||
opts.description += f". By : {self.contact_by}"
|
||||
|
||||
if self.to_discuss:
|
||||
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
|
||||
|
||||
super(Opportunity, self).add_calendar_event(opts, force)
|
||||
self.customer_name = customer_name
|
||||
elif self.opportunity_from == "Prospect":
|
||||
self.customer_name = self.party_name
|
||||
|
||||
def validate_item_details(self):
|
||||
if not self.get("items"):
|
||||
@ -294,7 +317,7 @@ def make_quotation(source_name, target_doc=None):
|
||||
|
||||
quotation.run_method("set_missing_values")
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
if not source.with_items:
|
||||
if not source.get("items", []):
|
||||
quotation.opportunity = source.name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
@ -398,15 +421,17 @@ def auto_close_opportunity():
|
||||
frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
|
||||
)
|
||||
|
||||
opportunities = frappe.db.sql(
|
||||
""" select name from tabOpportunity where status='Replied' and
|
||||
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """,
|
||||
(auto_close_after_days),
|
||||
as_dict=True,
|
||||
)
|
||||
table = frappe.qb.DocType("Opportunity")
|
||||
opportunities = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where(
|
||||
(table.modified < (Now() - Interval(days=auto_close_after_days))) & (table.status == "Replied")
|
||||
)
|
||||
).run(pluck=True)
|
||||
|
||||
for opportunity in opportunities:
|
||||
doc = frappe.get_doc("Opportunity", opportunity.get("name"))
|
||||
doc = frappe.get_doc("Opportunity", opportunity)
|
||||
doc.status = "Closed"
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
@ -437,34 +462,3 @@ def make_opportunity_from_communication(communication, company, ignore_communica
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
return opportunity.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
:param start: Start date-time.
|
||||
:param end: End date-time.
|
||||
:param filters: Filters (JSON).
|
||||
"""
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
conditions = get_event_conditions("Opportunity", filters)
|
||||
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
distinct `tabOpportunity`.name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_amount,
|
||||
`tabOpportunity`.title, `tabOpportunity`.contact_date
|
||||
from
|
||||
`tabOpportunity`
|
||||
where
|
||||
(`tabOpportunity`.contact_date between %(start)s and %(end)s)
|
||||
{conditions}
|
||||
""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
{"start": start, "end": end},
|
||||
as_dict=True,
|
||||
update={"allDay": 0},
|
||||
)
|
||||
return data
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
frappe.views.calendar["Opportunity"] = {
|
||||
field_map: {
|
||||
"start": "contact_date",
|
||||
"end": "contact_date",
|
||||
"id": "name",
|
||||
"title": "customer_name",
|
||||
"allDay": "allDay"
|
||||
},
|
||||
options: {
|
||||
header: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'month'
|
||||
}
|
||||
},
|
||||
get_events_method: 'erpnext.crm.doctype.opportunity.opportunity.get_events'
|
||||
}
|
@ -77,42 +77,6 @@ class TestOpportunity(unittest.TestCase):
|
||||
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
|
||||
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
|
||||
|
||||
quotation_doc = make_quotation(opp_doc.name)
|
||||
quotation_doc.append("items", {"item_code": "_Test Item", "qty": 1})
|
||||
quotation_doc.run_method("set_missing_values")
|
||||
quotation_doc.run_method("calculate_taxes_and_totals")
|
||||
quotation_doc.save()
|
||||
|
||||
quotation_comment_count = frappe.db.count(
|
||||
"Comment",
|
||||
{
|
||||
"reference_doctype": quotation_doc.doctype,
|
||||
"reference_name": quotation_doc.name,
|
||||
"comment_type": "Comment",
|
||||
},
|
||||
)
|
||||
quotation_communication_count = len(
|
||||
get_linked_communication_list(quotation_doc.doctype, quotation_doc.name)
|
||||
)
|
||||
self.assertEqual(quotation_comment_count, 4)
|
||||
self.assertEqual(quotation_communication_count, 4)
|
||||
|
||||
def test_render_template_for_to_discuss(self):
|
||||
doc = make_opportunity(with_items=0, opportunity_from="Lead")
|
||||
doc.contact_by = "test@example.com"
|
||||
doc.contact_date = add_days(today(), days=2)
|
||||
doc.to_discuss = "{{ doc.name }} test data"
|
||||
doc.save()
|
||||
|
||||
event = frappe.get_all(
|
||||
"Event Participants",
|
||||
fields=["parent"],
|
||||
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
|
||||
)
|
||||
|
||||
event_description = frappe.db.get_value("Event", event[0].parent, "description")
|
||||
self.assertTrue(doc.name in event_description)
|
||||
|
||||
|
||||
def make_opportunity_from_lead():
|
||||
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
||||
@ -139,7 +103,6 @@ def make_opportunity(**args):
|
||||
"opportunity_from": args.opportunity_from or "Customer",
|
||||
"opportunity_type": "Sales",
|
||||
"conversion_rate": 1.0,
|
||||
"with_items": args.with_items or 0,
|
||||
"transaction_date": today(),
|
||||
}
|
||||
)
|
||||
|
@ -8,7 +8,9 @@
|
||||
"transaction_date": "2013-12-12",
|
||||
"items": [{
|
||||
"item_name": "Test Item",
|
||||
"description": "Some description"
|
||||
"description": "Some description",
|
||||
"qty": 5,
|
||||
"rate": 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
@ -27,5 +27,26 @@ frappe.ui.form.on('Prospect', {
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
frm.trigger("show_notes");
|
||||
frm.trigger("show_activities");
|
||||
},
|
||||
|
||||
show_notes (frm) {
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: frm,
|
||||
notes_wrapper: $(frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
},
|
||||
|
||||
show_activities (frm) {
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: frm,
|
||||
open_activities_wrapper: $(frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -1,33 +1,42 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "field:company_name",
|
||||
"creation": "2021-08-19 00:21:06.995448",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"overview_tab",
|
||||
"company_name",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"customer_group",
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"column_break_4",
|
||||
"market_segment",
|
||||
"industry",
|
||||
"territory",
|
||||
"column_break_6",
|
||||
"no_of_employees",
|
||||
"currency",
|
||||
"annual_revenue",
|
||||
"more_details_section",
|
||||
"fax",
|
||||
"website",
|
||||
"column_break_13",
|
||||
"prospect_owner",
|
||||
"website",
|
||||
"fax",
|
||||
"company",
|
||||
"leads_section",
|
||||
"prospect_lead",
|
||||
"address_and_contact_section",
|
||||
"column_break_16",
|
||||
"contacts_tab",
|
||||
"address_html",
|
||||
"column_break_17",
|
||||
"column_break_18",
|
||||
"contact_html",
|
||||
"leads_section",
|
||||
"leads",
|
||||
"opportunities_tab",
|
||||
"opportunities",
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_section",
|
||||
"notes_html",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
@ -71,15 +80,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
"label": "No. of Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
"fieldtype": "Select",
|
||||
"label": "No. of Employees",
|
||||
"options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
@ -97,8 +100,7 @@
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website",
|
||||
"options": "URL"
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_owner",
|
||||
@ -108,23 +110,14 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "leads_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Leads"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_lead",
|
||||
"fieldtype": "Table",
|
||||
"options": "Prospect Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
@ -132,28 +125,16 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text Editor"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"label": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@ -161,11 +142,83 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Opportunities"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Opportunities",
|
||||
"options": "Prospect Opportunity"
|
||||
},
|
||||
{
|
||||
"fieldname": "contacts_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "leads",
|
||||
"fieldtype": "Table",
|
||||
"options": "Prospect Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "overview_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Overview"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-01 13:10:36.759249",
|
||||
"modified": "2022-06-21 15:10:26.887502",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect",
|
||||
@ -207,6 +260,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -3,19 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
|
||||
|
||||
class Prospect(Document):
|
||||
class Prospect(CRMNote):
|
||||
def onload(self):
|
||||
load_address_and_contact(self)
|
||||
|
||||
def validate(self):
|
||||
self.update_lead_details()
|
||||
|
||||
def on_update(self):
|
||||
self.link_with_lead_contact_and_address()
|
||||
|
||||
@ -23,23 +19,24 @@ class Prospect(Document):
|
||||
self.unlink_dynamic_links()
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
for row in self.get("prospect_lead"):
|
||||
copy_comments("Lead", row.lead, self)
|
||||
add_link_in_communication("Lead", row.lead, self)
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
)
|
||||
|
||||
def update_lead_details(self):
|
||||
for row in self.get("prospect_lead"):
|
||||
lead = frappe.get_value(
|
||||
"Lead", row.lead, ["lead_name", "status", "email_id", "mobile_no"], as_dict=True
|
||||
)
|
||||
row.lead_name = lead.lead_name
|
||||
row.status = lead.status
|
||||
row.email = lead.email_id
|
||||
row.mobile_no = lead.mobile_no
|
||||
for row in self.get("leads"):
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Lead", row.lead, self)
|
||||
link_communications("Lead", row.lead, self)
|
||||
link_open_events("Lead", row.lead, self)
|
||||
|
||||
for row in self.get("opportunities"):
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Opportunity", row.opportunity, self)
|
||||
link_communications("Opportunity", row.opportunity, self)
|
||||
link_open_events("Opportunity", row.opportunity, self)
|
||||
|
||||
def link_with_lead_contact_and_address(self):
|
||||
for row in self.prospect_lead:
|
||||
for row in self.leads:
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": "Lead", "link_name": row.lead},
|
||||
@ -116,9 +113,7 @@ def make_opportunity(source_name, target_doc=None):
|
||||
{
|
||||
"Prospect": {
|
||||
"doctype": "Opportunity",
|
||||
"field_map": {
|
||||
"name": "party_name",
|
||||
},
|
||||
"field_map": {"name": "party_name", "prospect_owner": "opportunity_owner"},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
@ -127,3 +122,25 @@ def make_opportunity(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opportunities(prospect):
|
||||
return frappe.get_all(
|
||||
"Opportunity",
|
||||
filters={"opportunity_from": "Prospect", "party_name": prospect},
|
||||
fields=[
|
||||
"opportunity_owner",
|
||||
"sales_stage",
|
||||
"status",
|
||||
"expected_closing",
|
||||
"probability",
|
||||
"opportunity_amount",
|
||||
"currency",
|
||||
"contact_person",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"creation",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ class TestProspect(unittest.TestCase):
|
||||
add_lead_to_prospect(lead_doc.name, prospect_doc.name)
|
||||
prospect_doc.reload()
|
||||
lead_exists_in_prosoect = False
|
||||
for rec in prospect_doc.get("prospect_lead"):
|
||||
for rec in prospect_doc.get("leads"):
|
||||
if rec.lead == lead_doc.name:
|
||||
lead_exists_in_prosoect = True
|
||||
self.assertEqual(lead_exists_in_prosoect, True)
|
||||
|
@ -7,12 +7,15 @@
|
||||
"field_order": [
|
||||
"lead",
|
||||
"lead_name",
|
||||
"status",
|
||||
"email",
|
||||
"mobile_no"
|
||||
"column_break_4",
|
||||
"mobile_no",
|
||||
"lead_owner",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "lead",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -21,6 +24,8 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.lead_name",
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -28,14 +33,17 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "lead.status",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.email_id",
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -44,18 +52,32 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.mobile_no",
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "lead.lead_owner",
|
||||
"fieldname": "lead_owner",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Lead Owner"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-25 12:58:24.638054",
|
||||
"modified": "2022-04-28 20:27:58.805970",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect Lead",
|
||||
@ -63,5 +85,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-04-27 17:40:37.965161",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"opportunity",
|
||||
"amount",
|
||||
"stage",
|
||||
"deal_owner",
|
||||
"column_break_4",
|
||||
"probability",
|
||||
"expected_closing",
|
||||
"currency",
|
||||
"contact_person"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "opportunity",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Opportunity",
|
||||
"options": "Opportunity"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.opportunity_amount",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.sales_stage",
|
||||
"fieldname": "stage",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Stage"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "opportunity.probability",
|
||||
"fieldname": "probability",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Probability"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "opportunity.expected_closing",
|
||||
"fieldname": "expected_closing",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Closing"
|
||||
},
|
||||
{
|
||||
"fetch_from": "opportunity.currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.opportunity_owner",
|
||||
"fieldname": "deal_owner",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Deal Owner"
|
||||
},
|
||||
{
|
||||
"fetch_from": "opportunity.contact_person",
|
||||
"fieldname": "contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Contact Person",
|
||||
"options": "Contact"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-28 10:05:38.730368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect Opportunity",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProspectOpportunity(Document):
|
||||
pass
|
@ -57,11 +57,5 @@ frappe.query_reports["Lost Opportunity"] = {
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "opportunity_from"
|
||||
},
|
||||
{
|
||||
"fieldname":"contact_by",
|
||||
"label": __("Next Contact By"),
|
||||
"fieldtype": "Link",
|
||||
"options": "User"
|
||||
},
|
||||
]
|
||||
};
|
||||
|
@ -7,8 +7,8 @@
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
|
||||
"modified": "2020-07-29 15:49:02.848845",
|
||||
"json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
|
||||
"modified": "2022-06-04 15:49:02.848845",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lost Opportunity",
|
||||
|
@ -61,13 +61,6 @@ def get_columns():
|
||||
"options": "Territory",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Next Contact By"),
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": 150,
|
||||
},
|
||||
]
|
||||
return columns
|
||||
|
||||
@ -81,7 +74,6 @@ def get_data(filters):
|
||||
`tabOpportunity`.party_name,
|
||||
`tabOpportunity`.customer_name,
|
||||
`tabOpportunity`.opportunity_type,
|
||||
`tabOpportunity`.contact_by,
|
||||
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
|
||||
`tabOpportunity`.sales_stage,
|
||||
`tabOpportunity`.territory
|
||||
@ -115,9 +107,6 @@ def get_conditions(filters):
|
||||
if filters.get("party_name"):
|
||||
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
|
||||
|
||||
if filters.get("contact_by"):
|
||||
conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s")
|
||||
|
||||
return " ".join(conditions) if conditions else ""
|
||||
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, now, today
|
||||
from pypika import functions
|
||||
|
||||
|
||||
def update_lead_phone_numbers(contact, method):
|
||||
@ -41,7 +44,7 @@ def copy_comments(doctype, docname, doc):
|
||||
comment.insert()
|
||||
|
||||
|
||||
def add_link_in_communication(doctype, docname, doc):
|
||||
def link_communications(doctype, docname, doc):
|
||||
communication_list = get_linked_communication_list(doctype, docname)
|
||||
|
||||
for communication in communication_list:
|
||||
@ -60,3 +63,159 @@ def get_linked_communication_list(doctype, docname):
|
||||
)
|
||||
|
||||
return communications + communication_links
|
||||
|
||||
|
||||
def link_communications_with_prospect(communication, method):
|
||||
prospect = get_linked_prospect(communication.reference_doctype, communication.reference_name)
|
||||
|
||||
if prospect:
|
||||
already_linked = any(
|
||||
[
|
||||
d.name
|
||||
for d in communication.get("timeline_links")
|
||||
if d.link_doctype == "Prospect" and d.link_name == prospect
|
||||
]
|
||||
)
|
||||
if not already_linked:
|
||||
row = communication.append("timeline_links")
|
||||
row.link_doctype = "Prospect"
|
||||
row.link_name = prospect
|
||||
row.db_update()
|
||||
|
||||
|
||||
def get_linked_prospect(reference_doctype, reference_name):
|
||||
prospect = None
|
||||
if reference_doctype == "Lead":
|
||||
prospect = frappe.db.get_value("Prospect Lead", {"lead": reference_name}, "parent")
|
||||
|
||||
elif reference_doctype == "Opportunity":
|
||||
opportunity_from, party_name = frappe.db.get_value(
|
||||
"Opportunity", reference_name, ["opportunity_from", "party_name"]
|
||||
)
|
||||
if opportunity_from == "Lead":
|
||||
prospect = frappe.db.get_value(
|
||||
"Prospect Opportunity", {"opportunity": reference_name}, "parent"
|
||||
)
|
||||
if opportunity_from == "Prospect":
|
||||
prospect = party_name
|
||||
|
||||
return prospect
|
||||
|
||||
|
||||
def link_events_with_prospect(event, method):
|
||||
if event.event_participants:
|
||||
ref_doctype = event.event_participants[0].reference_doctype
|
||||
ref_docname = event.event_participants[0].reference_docname
|
||||
prospect = get_linked_prospect(ref_doctype, ref_docname)
|
||||
if prospect:
|
||||
event.add_participant("Prospect", prospect)
|
||||
event.save()
|
||||
|
||||
|
||||
def link_open_tasks(ref_doctype, ref_docname, doc):
|
||||
todos = get_open_todos(ref_doctype, ref_docname)
|
||||
|
||||
for todo in todos:
|
||||
todo_doc = frappe.get_doc("ToDo", todo.name)
|
||||
todo_doc.reference_type = doc.doctype
|
||||
todo_doc.reference_name = doc.name
|
||||
todo_doc.db_update()
|
||||
|
||||
|
||||
def link_open_events(ref_doctype, ref_docname, doc):
|
||||
events = get_open_events(ref_doctype, ref_docname)
|
||||
for event in events:
|
||||
event_doc = frappe.get_doc("Event", event.name)
|
||||
event_doc.add_participant(doc.doctype, doc.name)
|
||||
event_doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_activities(ref_doctype, ref_docname):
|
||||
tasks = get_open_todos(ref_doctype, ref_docname)
|
||||
events = get_open_events(ref_doctype, ref_docname)
|
||||
|
||||
return {"tasks": tasks, "events": events}
|
||||
|
||||
|
||||
def get_open_todos(ref_doctype, ref_docname):
|
||||
return frappe.get_all(
|
||||
"ToDo",
|
||||
filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": "Open"},
|
||||
fields=[
|
||||
"name",
|
||||
"description",
|
||||
"allocated_to",
|
||||
"date",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_open_events(ref_doctype, ref_docname):
|
||||
event = frappe.qb.DocType("Event")
|
||||
event_link = frappe.qb.DocType("Event Participants")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(event)
|
||||
.join(event_link)
|
||||
.on(event_link.parent == event.name)
|
||||
.select(
|
||||
event.name,
|
||||
event.subject,
|
||||
event.event_category,
|
||||
event.starts_on,
|
||||
event.ends_on,
|
||||
event.description,
|
||||
)
|
||||
.where(
|
||||
(event_link.reference_doctype == ref_doctype)
|
||||
& (event_link.reference_docname == ref_docname)
|
||||
& (event.status == "Open")
|
||||
)
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def open_leads_opportunities_based_on_todays_event():
|
||||
event = frappe.qb.DocType("Event")
|
||||
event_link = frappe.qb.DocType("Event Participants")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(event)
|
||||
.join(event_link)
|
||||
.on(event_link.parent == event.name)
|
||||
.select(event_link.reference_doctype, event_link.reference_docname)
|
||||
.where(
|
||||
(event_link.reference_doctype.isin(["Lead", "Opportunity"]))
|
||||
& (event.status == "Open")
|
||||
& (functions.Date(event.starts_on) == today())
|
||||
)
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
for d in data:
|
||||
frappe.db.set_value(d.reference_doctype, d.reference_docname, "status", "Open")
|
||||
|
||||
|
||||
class CRMNote(Document):
|
||||
@frappe.whitelist()
|
||||
def add_note(self, note):
|
||||
self.append("notes", {"note": note, "added_by": frappe.session.user, "added_on": now()})
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_note(self, note, row_id):
|
||||
for d in self.notes:
|
||||
if cstr(d.name) == row_id:
|
||||
d.note = note
|
||||
d.db_update()
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_note(self, row_id):
|
||||
for d in self.notes:
|
||||
if cstr(d.name) == row_id:
|
||||
self.remove(d)
|
||||
break
|
||||
self.save()
|
||||
|
@ -1,44 +0,0 @@
|
||||
import frappe
|
||||
from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
|
||||
from github import Github
|
||||
|
||||
class GithubConnection(BaseConnection):
|
||||
def __init__(self, connector):
|
||||
self.connector = connector
|
||||
|
||||
try:
|
||||
password = self.get_password()
|
||||
except frappe.AuthenticationError:
|
||||
password = None
|
||||
|
||||
if self.connector.username and password:
|
||||
self.connection = Github(self.connector.username, self.get_password())
|
||||
else:
|
||||
self.connection = Github()
|
||||
|
||||
self.name_field = 'id'
|
||||
|
||||
def insert(self, doctype, doc):
|
||||
pass
|
||||
|
||||
def update(self, doctype, doc, migration_id):
|
||||
pass
|
||||
|
||||
def delete(self, doctype, migration_id):
|
||||
pass
|
||||
|
||||
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
|
||||
repo = filters.get('repo')
|
||||
|
||||
if remote_objectname == 'Milestone':
|
||||
return self.get_milestones(repo, start, page_length)
|
||||
if remote_objectname == 'Issue':
|
||||
return self.get_issues(repo, start, page_length)
|
||||
|
||||
def get_milestones(self, repo, start=0, page_length=10):
|
||||
_repo = self.connection.get_repo(repo)
|
||||
return list(_repo.get_milestones()[start:start+page_length])
|
||||
|
||||
def get_issues(self, repo, start=0, page_length=10):
|
||||
_repo = self.connection.get_repo(repo)
|
||||
return list(_repo.get_issues()[start:start+page_length])
|
@ -1,12 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def pre_process(issue):
|
||||
|
||||
project = frappe.db.get_value("Project", filters={"project_name": issue.milestone})
|
||||
return {
|
||||
"title": issue.title,
|
||||
"body": frappe.utils.md_to_html(issue.body or ""),
|
||||
"state": issue.state.title(),
|
||||
"project": project or "",
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"condition": "{\"repo\":\"frappe/erpnext\"}",
|
||||
"creation": "2017-10-16 16:03:32.772191",
|
||||
"docstatus": 0,
|
||||
"doctype": "Data Migration Mapping",
|
||||
"fields": [
|
||||
{
|
||||
"is_child_table": 0,
|
||||
"local_fieldname": "subject",
|
||||
"remote_fieldname": "title"
|
||||
},
|
||||
{
|
||||
"is_child_table": 0,
|
||||
"local_fieldname": "description",
|
||||
"remote_fieldname": "body"
|
||||
},
|
||||
{
|
||||
"is_child_table": 0,
|
||||
"local_fieldname": "status",
|
||||
"remote_fieldname": "state"
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"local_doctype": "Task",
|
||||
"local_primary_key": "name",
|
||||
"mapping_name": "Issue to Task",
|
||||
"mapping_type": "Pull",
|
||||
"migration_id_field": "github_sync_id",
|
||||
"modified": "2017-10-20 11:48:54.575993",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Issue to Task",
|
||||
"owner": "Administrator",
|
||||
"page_length": 10,
|
||||
"remote_objectname": "Issue",
|
||||
"remote_primary_key": "id"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
def pre_process(milestone):
|
||||
return {
|
||||
"title": milestone.title,
|
||||
"description": milestone.description,
|
||||
"state": milestone.state.title(),
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"condition": "{\"repo\": \"frappe/erpnext\"}",
|
||||
"creation": "2017-10-13 11:16:49.664925",
|
||||
"docstatus": 0,
|
||||
"doctype": "Data Migration Mapping",
|
||||
"fields": [
|
||||
{
|
||||
"is_child_table": 0,
|
||||
"local_fieldname": "project_name",
|
||||
"remote_fieldname": "title"
|
||||
},
|
||||
{
|
||||
"is_child_table": 0,
|
||||
"local_fieldname": "notes",
|
||||
"remote_fieldname": "description"
|
||||
},
|
||||
{
|
||||
"is_child_table": 0,
|
||||
"local_fieldname": "status",
|
||||
"remote_fieldname": "state"
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"local_doctype": "Project",
|
||||
"local_primary_key": "project_name",
|
||||
"mapping_name": "Milestone to Project",
|
||||
"mapping_type": "Pull",
|
||||
"migration_id_field": "github_sync_id",
|
||||
"modified": "2017-10-20 11:48:54.552305",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Milestone to Project",
|
||||
"owner": "Administrator",
|
||||
"page_length": 10,
|
||||
"remote_objectname": "Milestone",
|
||||
"remote_primary_key": "id"
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"creation": "2017-10-13 11:16:53.600026",
|
||||
"docstatus": 0,
|
||||
"doctype": "Data Migration Plan",
|
||||
"idx": 0,
|
||||
"mappings": [
|
||||
{
|
||||
"enabled": 1,
|
||||
"mapping": "Milestone to Project"
|
||||
},
|
||||
{
|
||||
"enabled": 1,
|
||||
"mapping": "Issue to Task"
|
||||
}
|
||||
],
|
||||
"modified": "2017-10-20 11:48:54.496123",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "GitHub Sync",
|
||||
"owner": "Administrator",
|
||||
"plan_name": "GitHub Sync"
|
||||
}
|
@ -23,7 +23,7 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.sql("delete from `tabMpesa Settings`")
|
||||
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
|
||||
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
|
||||
|
||||
def test_creation_of_payment_gateway(self):
|
||||
mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone")
|
||||
|
@ -300,7 +300,11 @@ doc_events = {
|
||||
"on_update": [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
|
||||
"erpnext.support.doctype.issue.issue.set_first_response_time",
|
||||
]
|
||||
],
|
||||
"after_insert": "erpnext.crm.utils.link_communications_with_prospect",
|
||||
},
|
||||
"Event": {
|
||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
||||
},
|
||||
"Sales Taxes and Charges Template": {
|
||||
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
|
||||
@ -391,9 +395,12 @@ auto_cancel_exempted_doctypes = [
|
||||
|
||||
scheduler_events = {
|
||||
"cron": {
|
||||
"0/5 * * * *": [
|
||||
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||
],
|
||||
"0/30 * * * *": [
|
||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||
]
|
||||
],
|
||||
},
|
||||
"all": [
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
@ -439,7 +446,7 @@ scheduler_events = {
|
||||
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
|
||||
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
||||
"erpnext.crm.doctype.lead.lead.daily_open_lead",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
|
@ -70,7 +70,7 @@ class Loan(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.unlink_loan_security_pledge()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
|
||||
def set_missing_fields(self):
|
||||
if not self.company:
|
||||
|
@ -29,7 +29,7 @@ class LoanDisbursement(AccountsController):
|
||||
def on_cancel(self):
|
||||
self.set_status_and_amounts(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.disbursement_date:
|
||||
|
@ -32,7 +32,7 @@ class LoanInterestAccrual(AccountsController):
|
||||
self.update_is_accrued()
|
||||
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
|
||||
def update_is_accrued(self):
|
||||
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
|
||||
|
@ -41,7 +41,7 @@ class LoanRepayment(AccountsController):
|
||||
self.check_future_accruals()
|
||||
self.update_repayment_schedule(cancel=1)
|
||||
self.mark_as_unpaid()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
self.make_gl_entries(cancel=1)
|
||||
|
||||
def set_missing_values(self, amounts):
|
||||
|
@ -42,7 +42,7 @@ class LoanWriteOff(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_outstanding_amount(cancel=1)
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
self.make_gl_entries(cancel=1)
|
||||
|
||||
def update_outstanding_amount(self, cancel=0):
|
||||
|
@ -81,7 +81,7 @@ frappe.ui.form.on("BOM", {
|
||||
}
|
||||
)
|
||||
|
||||
if (!frm.doc.__islocal && frm.doc.docstatus<2) {
|
||||
if (!frm.is_new() && frm.doc.docstatus<2) {
|
||||
frm.add_custom_button(__("Update Cost"), function() {
|
||||
frm.events.update_cost(frm, true);
|
||||
});
|
||||
@ -93,10 +93,12 @@ frappe.ui.form.on("BOM", {
|
||||
});
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("New Version"), function() {
|
||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||
frappe.set_route("Form", "BOM", new_bom.name);
|
||||
});
|
||||
if (!frm.is_new() && !frm.doc.docstatus == 0) {
|
||||
frm.add_custom_button(__("New Version"), function() {
|
||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||
frappe.set_route("Form", "BOM", new_bom.name);
|
||||
});
|
||||
}
|
||||
|
||||
if(frm.doc.docstatus==1) {
|
||||
frm.add_custom_button(__("Work Order"), function() {
|
||||
|
@ -1,11 +1,11 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import functools
|
||||
import re
|
||||
from collections import deque
|
||||
from operator import itemgetter
|
||||
from typing import List
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@ -189,6 +189,7 @@ class BOM(WebsiteGenerator):
|
||||
self.validate_transfer_against()
|
||||
self.set_routing_operations()
|
||||
self.validate_operations()
|
||||
self.update_exploded_items(save=False)
|
||||
self.calculate_cost()
|
||||
self.update_stock_qty()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||
@ -386,40 +387,14 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
existing_bom_cost = self.total_cost
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.item_code:
|
||||
continue
|
||||
|
||||
rate = self.get_rm_rate(
|
||||
{
|
||||
"company": self.company,
|
||||
"item_code": d.item_code,
|
||||
"bom_no": d.bom_no,
|
||||
"qty": d.qty,
|
||||
"uom": d.uom,
|
||||
"stock_uom": d.stock_uom,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"sourced_by_supplier": d.sourced_by_supplier,
|
||||
}
|
||||
)
|
||||
|
||||
if rate:
|
||||
d.rate = rate
|
||||
d.amount = flt(d.rate) * flt(d.qty)
|
||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
||||
d.base_amount = flt(d.amount) * flt(self.conversion_rate)
|
||||
|
||||
if save:
|
||||
d.db_update()
|
||||
|
||||
if self.docstatus == 1:
|
||||
self.flags.ignore_validate_update_after_submit = True
|
||||
self.calculate_cost(update_hour_rate)
|
||||
|
||||
self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate)
|
||||
|
||||
if save:
|
||||
self.db_update()
|
||||
|
||||
self.update_exploded_items(save=save)
|
||||
|
||||
# update parent BOMs
|
||||
if self.total_cost != existing_bom_cost and update_parent:
|
||||
parent_boms = frappe.db.sql_list(
|
||||
@ -470,6 +445,7 @@ class BOM(WebsiteGenerator):
|
||||
and self.is_active
|
||||
):
|
||||
frappe.db.set(self, "is_default", 1)
|
||||
frappe.db.set_value("Item", self.item, "default_bom", self.name)
|
||||
else:
|
||||
frappe.db.set(self, "is_default", 0)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
@ -608,11 +584,15 @@ class BOM(WebsiteGenerator):
|
||||
bom_list.reverse()
|
||||
return bom_list
|
||||
|
||||
def calculate_cost(self, update_hour_rate=False):
|
||||
def calculate_cost(self, save_updates=False, update_hour_rate=False):
|
||||
"""Calculate bom totals"""
|
||||
self.calculate_op_cost(update_hour_rate)
|
||||
self.calculate_rm_cost()
|
||||
self.calculate_sm_cost()
|
||||
self.calculate_rm_cost(save=save_updates)
|
||||
self.calculate_sm_cost(save=save_updates)
|
||||
if save_updates:
|
||||
# not via doc event, table is not regenerated and needs updation
|
||||
self.calculate_exploded_cost()
|
||||
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||
self.base_total_cost = (
|
||||
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||
@ -654,12 +634,26 @@ class BOM(WebsiteGenerator):
|
||||
if update_hour_rate:
|
||||
row.db_update()
|
||||
|
||||
def calculate_rm_cost(self):
|
||||
def calculate_rm_cost(self, save=False):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_rm_cost = 0
|
||||
base_total_rm_cost = 0
|
||||
|
||||
for d in self.get("items"):
|
||||
old_rate = d.rate
|
||||
d.rate = self.get_rm_rate(
|
||||
{
|
||||
"company": self.company,
|
||||
"item_code": d.item_code,
|
||||
"bom_no": d.bom_no,
|
||||
"qty": d.qty,
|
||||
"uom": d.uom,
|
||||
"stock_uom": d.stock_uom,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"sourced_by_supplier": d.sourced_by_supplier,
|
||||
}
|
||||
)
|
||||
|
||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
||||
d.base_amount = d.amount * flt(self.conversion_rate)
|
||||
@ -669,11 +663,13 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
total_rm_cost += d.amount
|
||||
base_total_rm_cost += d.base_amount
|
||||
if save and (old_rate != d.rate):
|
||||
d.db_update()
|
||||
|
||||
self.raw_material_cost = total_rm_cost
|
||||
self.base_raw_material_cost = base_total_rm_cost
|
||||
|
||||
def calculate_sm_cost(self):
|
||||
def calculate_sm_cost(self, save=False):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_sm_cost = 0
|
||||
base_total_sm_cost = 0
|
||||
@ -688,10 +684,45 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
total_sm_cost += d.amount
|
||||
base_total_sm_cost += d.base_amount
|
||||
if save:
|
||||
d.db_update()
|
||||
|
||||
self.scrap_material_cost = total_sm_cost
|
||||
self.base_scrap_material_cost = base_total_sm_cost
|
||||
|
||||
def calculate_exploded_cost(self):
|
||||
"Set exploded row cost from it's parent BOM."
|
||||
rm_rate_map = self.get_rm_rate_map()
|
||||
|
||||
for row in self.get("exploded_items"):
|
||||
old_rate = flt(row.rate)
|
||||
row.rate = rm_rate_map.get(row.item_code)
|
||||
row.amount = flt(row.stock_qty) * flt(row.rate)
|
||||
|
||||
if old_rate != row.rate:
|
||||
# Only db_update if changed
|
||||
row.db_update()
|
||||
|
||||
def get_rm_rate_map(self) -> Dict[str, float]:
|
||||
"Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM."
|
||||
rm_rate_map = {}
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.bom_no:
|
||||
# Get Item-Rate from Subassembly BOM
|
||||
explosion_items = frappe.get_all(
|
||||
"BOM Explosion Item",
|
||||
filters={"parent": item.bom_no},
|
||||
fields=["item_code", "rate"],
|
||||
order_by=None, # to avoid sort index creation at db level (granular change)
|
||||
)
|
||||
explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items}
|
||||
rm_rate_map.update(explosion_item_rate)
|
||||
else:
|
||||
rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0)
|
||||
|
||||
return rm_rate_map
|
||||
|
||||
def update_exploded_items(self, save=True):
|
||||
"""Update Flat BOM, following will be correct data"""
|
||||
self.get_exploded_items()
|
||||
@ -902,44 +933,46 @@ def get_bom_item_rate(args, bom_doc):
|
||||
return flt(rate)
|
||||
|
||||
|
||||
def get_valuation_rate(args):
|
||||
"""Get weighted average of valuation rate from all warehouses"""
|
||||
def get_valuation_rate(data):
|
||||
"""
|
||||
1) Get average valuation rate from all warehouses
|
||||
2) If no value, get last valuation rate from SLE
|
||||
3) If no value, get valuation rate from Item
|
||||
"""
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
|
||||
item_bins = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
bin.actual_qty, bin.stock_value
|
||||
from
|
||||
`tabBin` bin, `tabWarehouse` warehouse
|
||||
where
|
||||
bin.item_code=%(item)s
|
||||
and bin.warehouse = warehouse.name
|
||||
and warehouse.company=%(company)s""",
|
||||
{"item": args["item_code"], "company": args["company"]},
|
||||
as_dict=1,
|
||||
)
|
||||
item_code, company = data.get("item_code"), data.get("company")
|
||||
valuation_rate = 0.0
|
||||
|
||||
for d in item_bins:
|
||||
total_qty += flt(d.actual_qty)
|
||||
total_value += flt(d.stock_value)
|
||||
bin_table = frappe.qb.DocType("Bin")
|
||||
wh_table = frappe.qb.DocType("Warehouse")
|
||||
item_valuation = (
|
||||
frappe.qb.from_(bin_table)
|
||||
.join(wh_table)
|
||||
.on(bin_table.warehouse == wh_table.name)
|
||||
.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
|
||||
.where((bin_table.item_code == item_code) & (wh_table.company == company))
|
||||
).run(as_dict=True)[0]
|
||||
|
||||
if total_qty:
|
||||
valuation_rate = total_value / total_qty
|
||||
valuation_rate = item_valuation.get("valuation_rate")
|
||||
|
||||
if valuation_rate <= 0:
|
||||
last_valuation_rate = frappe.db.sql(
|
||||
"""select valuation_rate
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %s and valuation_rate > 0 and is_cancelled = 0
|
||||
order by posting_date desc, posting_time desc, creation desc limit 1""",
|
||||
args["item_code"],
|
||||
)
|
||||
if (valuation_rate is not None) and valuation_rate <= 0:
|
||||
# Explicit null value check. If None, Bins don't exist, neither does SLE
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
last_val_rate = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.valuation_rate)
|
||||
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
|
||||
.orderby(sle.posting_date, order=frappe.qb.desc)
|
||||
.orderby(sle.posting_time, order=frappe.qb.desc)
|
||||
.orderby(sle.creation, order=frappe.qb.desc)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
|
||||
valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0
|
||||
|
||||
if not valuation_rate:
|
||||
valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
|
||||
valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
|
||||
|
||||
return flt(valuation_rate)
|
||||
|
||||
@ -1125,39 +1158,6 @@ def get_children(parent=None, is_root=False, **filters):
|
||||
return bom_items
|
||||
|
||||
|
||||
def get_boms_in_bottom_up_order(bom_no=None):
|
||||
def _get_parent(bom_no):
|
||||
return frappe.db.sql_list(
|
||||
"""
|
||||
select distinct bom_item.parent from `tabBOM Item` bom_item
|
||||
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
|
||||
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
|
||||
""",
|
||||
bom_no,
|
||||
)
|
||||
|
||||
count = 0
|
||||
bom_list = []
|
||||
if bom_no:
|
||||
bom_list.append(bom_no)
|
||||
else:
|
||||
# get all leaf BOMs
|
||||
bom_list = frappe.db.sql_list(
|
||||
"""select name from `tabBOM` bom
|
||||
where docstatus=1 and is_active=1
|
||||
and not exists(select bom_no from `tabBOM Item`
|
||||
where parent=bom.name and ifnull(bom_no, '')!='')"""
|
||||
)
|
||||
|
||||
while count < len(bom_list):
|
||||
for child_bom in _get_parent(bom_list[count]):
|
||||
if child_bom not in bom_list:
|
||||
bom_list.append(child_bom)
|
||||
count += 1
|
||||
|
||||
return bom_list
|
||||
|
||||
|
||||
def add_additional_cost(stock_entry, work_order):
|
||||
# Add non stock items cost in the additional cost
|
||||
stock_entry.additional_costs = []
|
||||
@ -1306,7 +1306,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not field in searchfields
|
||||
]
|
||||
|
||||
query_filters = {"disabled": 0, "ifnull(end_of_life, '5050-50-50')": (">", today())}
|
||||
query_filters = {"disabled": 0, "end_of_life": (">", today())}
|
||||
|
||||
or_cond_filters = {}
|
||||
if txt:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user