mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-26 05:58:22 +00:00
Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ea5cfc21 | ||
|
|
eb1081664a | ||
|
|
5b27642880 | ||
|
|
973611a356 | ||
|
|
4f9c28cd22 | ||
|
|
e16c14863b | ||
|
|
e1a5a7006f | ||
|
|
93940f30b7 | ||
|
|
189954eaf1 | ||
|
|
33ee01174b | ||
|
|
87ba196473 | ||
|
|
017729d545 | ||
|
|
2ef2057f44 | ||
|
|
04fdaaffbd | ||
|
|
fc051d143c | ||
|
|
851b8871b2 | ||
|
|
3ed42e180c | ||
|
|
3ca4f24d21 | ||
|
|
21d560cd19 | ||
|
|
4a7d75b5cc | ||
|
|
8f13b484a9 | ||
|
|
8ecca2a1cf | ||
|
|
fafb46eebd | ||
|
|
d53b197896 | ||
|
|
3dd3935e76 | ||
|
|
5c388a132f | ||
|
|
b0234489ca | ||
|
|
21336f1a2c | ||
|
|
888118e3e1 | ||
|
|
d9008779de | ||
|
|
c66c4e3aab | ||
|
|
2240aeb307 | ||
|
|
42d09448ee | ||
|
|
8049582652 | ||
|
|
1d5415f39d | ||
|
|
69780da099 | ||
|
|
8ddfc34c30 | ||
|
|
fb823b53d1 | ||
|
|
23dacbe9b2 | ||
|
|
0138595000 | ||
|
|
f45e8b9c16 | ||
|
|
e44783a3c5 | ||
|
|
85ad34672c | ||
|
|
71207a7dae | ||
|
|
71e14b3dbb | ||
|
|
41f1c11e85 | ||
|
|
4be554d8b4 | ||
|
|
3194e3a020 | ||
|
|
cacb0f6fde | ||
|
|
200ddbf66c | ||
|
|
20f2bef55f | ||
|
|
e68b08817e | ||
|
|
2561cf2072 | ||
|
|
abbbfe6240 | ||
|
|
4e1b2c6f8d | ||
|
|
c669dba691 | ||
|
|
8c183741bd | ||
|
|
2bbea63de1 | ||
|
|
baf014fc61 | ||
|
|
95e3dc9b81 | ||
|
|
703e4f4f5d | ||
|
|
bf2ebce6f4 | ||
|
|
65d24ea9ea | ||
|
|
6c9c3426f8 | ||
|
|
146d41ee81 | ||
|
|
7da461b862 | ||
|
|
8b57ecd8ef | ||
|
|
7d010adcd4 | ||
|
|
4af0a9b192 | ||
|
|
3bac2a88bd | ||
|
|
50a8907e8c | ||
|
|
42e25d4cdf | ||
|
|
4c1befab8f | ||
|
|
d928a5c3aa | ||
|
|
3bf7115cdc | ||
|
|
5541d68477 | ||
|
|
070df97663 | ||
|
|
58a6bbcf6d | ||
|
|
6650373c9f | ||
|
|
3c790c12f2 | ||
|
|
b875de6fb7 | ||
|
|
58e304f64b | ||
|
|
46171738c0 | ||
|
|
e7abdc34d0 | ||
|
|
fc103ab4ce | ||
|
|
ce2bf5fb1c | ||
|
|
59ab13c34f | ||
|
|
7ffa6b4fbd | ||
|
|
19203bb87d | ||
|
|
9792099cea | ||
|
|
c11d950fc5 | ||
|
|
53ec2a9268 | ||
|
|
a0fc8e252c | ||
|
|
632b67cbc8 | ||
|
|
b261242792 | ||
|
|
01ac54d65d | ||
|
|
0e57f4dd3c | ||
|
|
7339e447bb | ||
|
|
55a8be5cad | ||
|
|
3f62e854e5 | ||
|
|
697fcef98b | ||
|
|
e2c4e16d72 | ||
|
|
3355dc2a41 | ||
|
|
77b0c5f722 | ||
|
|
138133e11d | ||
|
|
582ed2c7f1 | ||
|
|
d934dda410 | ||
|
|
c774c14b13 | ||
|
|
f3b7eedb25 | ||
|
|
9d1fac19e5 | ||
|
|
bdb5cc8ad4 | ||
|
|
975010c987 | ||
|
|
1f6352532e | ||
|
|
aa7c81a0bc | ||
|
|
08729b49e9 | ||
|
|
a02469f09c | ||
|
|
4a8ce226f6 | ||
|
|
6f59fa9e5b | ||
|
|
f39ae9dbb1 | ||
|
|
4add1b4374 | ||
|
|
dffa682b80 | ||
|
|
74ffb1d59c | ||
|
|
2060a003c8 | ||
|
|
9d18b40fb0 | ||
|
|
25b3c7736b | ||
|
|
6a08d04706 | ||
|
|
cf14858909 | ||
|
|
7af03800c9 | ||
|
|
6a21d617ce | ||
|
|
48c2c82dc4 | ||
|
|
8b617fb75e | ||
|
|
79483cc90e | ||
|
|
81ef2babe9 | ||
|
|
043815e745 | ||
|
|
2cf871c21e | ||
|
|
0ccb5a34bd | ||
|
|
8372f038c0 | ||
|
|
a630db1b21 | ||
|
|
a59dffe42a | ||
|
|
32e5bbbb46 | ||
|
|
feb5d0089b | ||
|
|
059141df52 | ||
|
|
29e079d5d6 | ||
|
|
7737b9061f | ||
|
|
2077f6e89c | ||
|
|
dee82754ab | ||
|
|
e5055160fb | ||
|
|
a8ac2a088d |
13
.github/workflows/patch.yml
vendored
13
.github/workflows/patch.yml
vendored
@@ -43,9 +43,11 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: "gabrielfalcao/pyenv-action@v9"
|
||||
uses: "actions/setup-python@v4"
|
||||
with:
|
||||
versions: 3.10:latest, 3.7:latest
|
||||
python-version: |
|
||||
3.7
|
||||
3.10
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
@@ -92,7 +94,6 @@ jobs:
|
||||
- name: Install
|
||||
run: |
|
||||
pip install frappe-bench
|
||||
pyenv global $(pyenv versions | grep '3.10')
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
@@ -107,7 +108,6 @@ jobs:
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
|
||||
pyenv global $(pyenv versions | grep '3.7')
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench setup env
|
||||
bench setup env --python python3.7
|
||||
bench pip install -e ./apps/payments
|
||||
bench pip install -e ./apps/erpnext
|
||||
|
||||
@@ -132,9 +132,8 @@ jobs:
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
|
||||
pyenv global $(pyenv versions | grep '3.10')
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env
|
||||
bench -v setup env --python python3.10
|
||||
bench pip install -e ./apps/payments
|
||||
bench pip install -e ./apps/erpnext
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.27.0"
|
||||
__version__ = "14.28.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -50,13 +50,15 @@ class AccountingDimension(Document):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
|
||||
frappe.enqueue(
|
||||
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
|
||||
def set_fieldname_and_label(self):
|
||||
if not self.label:
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"book_tax_discount_loss",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"currency_exchange_section",
|
||||
@@ -61,7 +62,10 @@
|
||||
"acc_frozen_upto",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"report_settings_sb"
|
||||
"report_settings_sb",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -378,6 +382,32 @@
|
||||
"fieldname": "auto_reconcile_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Reconcile Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Auto match and set the Party in Bank Transactions",
|
||||
"fieldname": "enable_party_matching",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Automatic Party Matching"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "enable_party_matching",
|
||||
"description": "Approximately match the description/party name against parties",
|
||||
"fieldname": "enable_fuzzy_matching",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Fuzzy Matching"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -385,7 +415,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 13:11:37.130743",
|
||||
"modified": "2023-06-15 18:47:46.430291",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -414,4 +444,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
@@ -140,6 +141,9 @@ def create_journal_entry_bts(
|
||||
second_account
|
||||
)
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
|
||||
accounts = []
|
||||
# Multi Currency?
|
||||
accounts.append(
|
||||
@@ -149,6 +153,7 @@ def create_journal_entry_bts(
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -158,11 +163,10 @@ def create_journal_entry_bts(
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type": entry_type,
|
||||
"company": company,
|
||||
|
||||
178
erpnext/accounts/doctype/bank_transaction/auto_match_party.py
Normal file
178
erpnext/accounts/doctype/bank_transaction/auto_match_party.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from typing import Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
"""
|
||||
Matches by Account/IBAN and then by Party Name/Description sequentially.
|
||||
Returns when a result is obtained.
|
||||
|
||||
Result (if present) is of the form: (Party Type, Party,)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
result = None
|
||||
result = AutoMatchbyAccountIBAN(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
|
||||
if not result and fuzzy_matching_enabled:
|
||||
result = AutoMatchbyPartyNameDescription(
|
||||
bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
|
||||
).match()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AutoMatchbyAccountIBAN:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self):
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
return None
|
||||
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
|
||||
def match_account_in_party(self) -> Union[Tuple, None]:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
or_filters = self.get_or_filters()
|
||||
|
||||
for party in parties:
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
|
||||
)
|
||||
|
||||
if party == "Employee" and not party_result:
|
||||
# Search in Bank Accounts first for Employee, and then Employee record
|
||||
if "bank_account_no" in or_filters:
|
||||
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
|
||||
|
||||
party_result = frappe.db.get_all(
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
party_result[0],
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def get_or_filters(self) -> dict:
|
||||
or_filters = {}
|
||||
if self.bank_party_account_number:
|
||||
or_filters["bank_account_no"] = self.bank_party_account_number
|
||||
|
||||
if self.bank_party_iban:
|
||||
or_filters["iban"] = self.bank_party_iban
|
||||
|
||||
return or_filters
|
||||
|
||||
|
||||
class AutoMatchbyPartyNameDescription:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
# fuzzy search by customer/supplier & employee
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
|
||||
def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
|
||||
for party in parties:
|
||||
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
|
||||
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
|
||||
|
||||
for field in ["bank_party_name", "description"]:
|
||||
if not self.get(field):
|
||||
continue
|
||||
|
||||
result, skip = self.fuzzy_search_and_return_result(party, names, field)
|
||||
if result or skip:
|
||||
break
|
||||
|
||||
if result or skip:
|
||||
# Skip If: It was hard to distinguish between close matches and so match is None
|
||||
# OR if the right match was found
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
|
||||
skip = False
|
||||
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
if not party_name:
|
||||
return None, skip
|
||||
|
||||
return (
|
||||
party,
|
||||
party_name,
|
||||
), skip
|
||||
|
||||
def process_fuzzy_result(self, result: Union[list, None]):
|
||||
"""
|
||||
If there are multiple valid close matches return None as result may be faulty.
|
||||
Return the result only if one accurate match stands out.
|
||||
|
||||
Returns: Result, Skip (whether or not to discontinue matching)
|
||||
"""
|
||||
PARTY, SCORE, CUTOFF = 0, 1, 80
|
||||
|
||||
if not result or not len(result):
|
||||
return None, False
|
||||
|
||||
first_result = result[0]
|
||||
if len(result) == 1:
|
||||
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
|
||||
|
||||
second_result = result[1]
|
||||
if first_result[SCORE] > CUTOFF:
|
||||
# If multiple matches with the same score, return None but discontinue matching
|
||||
# Matches were found but were too close to distinguish between
|
||||
if first_result[SCORE] == second_result[SCORE]:
|
||||
return None, True
|
||||
|
||||
return first_result[PARTY], True
|
||||
else:
|
||||
return None, False
|
||||
|
||||
|
||||
def get_parties_in_order(deposit: float) -> list:
|
||||
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
|
||||
if flt(deposit) > 0:
|
||||
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
|
||||
|
||||
return parties
|
||||
@@ -33,7 +33,11 @@
|
||||
"unallocated_amount",
|
||||
"party_section",
|
||||
"party_type",
|
||||
"party"
|
||||
"party",
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -63,7 +67,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nPending\nSettled\nUnreconciled\nReconciled"
|
||||
"options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_account",
|
||||
@@ -202,11 +206,30 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction Type",
|
||||
"length": 50
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3czf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name/Account Holder (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Account No. (Bank Statement)"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-29 18:36:50.475964",
|
||||
"modified": "2023-06-06 13:58:12.821411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -260,4 +283,4 @@
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ class BankTransaction(StatusUpdater):
|
||||
self.clear_linked_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
_saving_flag = False
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
@@ -146,6 +149,26 @@ class BankTransaction(StatusUpdater):
|
||||
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||
)
|
||||
|
||||
def auto_set_party(self):
|
||||
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
|
||||
|
||||
if self.party_type and self.party:
|
||||
return
|
||||
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
if result:
|
||||
party_type, party = result
|
||||
frappe.db.set_value(
|
||||
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
|
||||
class TestAutoMatchParty(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_bank_account()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
|
||||
return super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_party_name(self):
|
||||
create_supplier_for_match(supplier_name="Jackson Ella W.")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban="DE04000000003716545346",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
def test_match_by_description(self):
|
||||
create_supplier_for_match(supplier_name="Microsoft")
|
||||
doc = create_bank_transaction(
|
||||
description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536",
|
||||
withdrawal=1200,
|
||||
transaction_id="8df880a2d09c3bed3fea358ca5168c5a",
|
||||
party_name="",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Microsoft")
|
||||
|
||||
def test_skip_match_if_multiple_close_results(self):
|
||||
create_supplier_for_match(supplier_name="Adithya Medical & General Stores")
|
||||
create_supplier_for_match(supplier_name="Adithya Medical And General Stores")
|
||||
|
||||
doc = create_bank_transaction(
|
||||
description="Paracetamol Consignment, SINV-0009",
|
||||
withdrawal=24.85,
|
||||
transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9",
|
||||
party_name="Adithya Medical & General",
|
||||
)
|
||||
|
||||
# Mapping is skipped as both Supplier names have the same match score
|
||||
self.assertEqual(doc.party_type, None)
|
||||
self.assertEqual(doc.party, None)
|
||||
|
||||
|
||||
def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None):
|
||||
if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
|
||||
# Update related Bank Account details
|
||||
if not (iban or account_no):
|
||||
return
|
||||
|
||||
frappe.db.set_value(
|
||||
dt="Bank Account",
|
||||
dn={"party": supplier_name},
|
||||
field={"iban": iban, "bank_account_no": account_no},
|
||||
)
|
||||
return
|
||||
|
||||
# Create Supplier and Bank Account for the same
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
supplier.supplier_group = "Services"
|
||||
supplier.supplier_type = "Company"
|
||||
supplier.insert()
|
||||
|
||||
if not frappe.db.exists("Bank", "TestBank"):
|
||||
bank = frappe.new_doc("Bank")
|
||||
bank.bank_name = "TestBank"
|
||||
bank.insert(ignore_if_duplicate=True)
|
||||
|
||||
if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"):
|
||||
bank_account = frappe.new_doc("Bank Account")
|
||||
bank_account.account_name = supplier.name
|
||||
bank_account.bank = "TestBank"
|
||||
bank_account.iban = iban
|
||||
bank_account.bank_account_no = account_no
|
||||
bank_account.party_type = "Supplier"
|
||||
bank_account.party = supplier.name
|
||||
bank_account.insert()
|
||||
|
||||
|
||||
def create_bank_transaction(
|
||||
description=None,
|
||||
withdrawal=0,
|
||||
deposit=0,
|
||||
transaction_id=None,
|
||||
party_name=None,
|
||||
account_no=None,
|
||||
iban=None,
|
||||
):
|
||||
doc = frappe.new_doc("Bank Transaction")
|
||||
doc.update(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
|
||||
"date": nowdate(),
|
||||
"withdrawal": withdrawal,
|
||||
"deposit": deposit,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"transaction_id": transaction_id,
|
||||
"bank_party_name": party_name,
|
||||
"bank_party_account_number": account_no,
|
||||
"bank_party_iban": iban,
|
||||
}
|
||||
)
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
doc.reload()
|
||||
|
||||
return doc
|
||||
@@ -35,6 +35,21 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
}
|
||||
},
|
||||
|
||||
validate_rounding_loss: function(frm) {
|
||||
let allowance = frm.doc.rounding_loss_allowance;
|
||||
if (!(allowance >= 0 && allowance < 1)) {
|
||||
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
|
||||
}
|
||||
},
|
||||
|
||||
rounding_loss_allowance: function(frm) {
|
||||
frm.events.validate_rounding_loss(frm);
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
frm.events.validate_rounding_loss(frm);
|
||||
},
|
||||
|
||||
get_entries: function(frm, account) {
|
||||
frappe.call({
|
||||
method: "get_accounts_data",
|
||||
@@ -126,7 +141,8 @@ var get_account_details = function(frm, cdt, cdn) {
|
||||
company: frm.doc.company,
|
||||
posting_date: frm.doc.posting_date,
|
||||
party_type: row.party_type,
|
||||
party: row.party
|
||||
party: row.party,
|
||||
rounding_loss_allowance: frm.doc.rounding_loss_allowance
|
||||
},
|
||||
callback: function(r){
|
||||
$.extend(row, r.message);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"rounding_loss_allowance",
|
||||
"column_break_2",
|
||||
"company",
|
||||
"section_break_4",
|
||||
@@ -96,11 +97,19 @@
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0.05",
|
||||
"description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
||||
"fieldname": "rounding_loss_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rounding Loss Allowance",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-29 19:38:24.416529",
|
||||
"modified": "2023-06-20 07:29:06.972434",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation",
|
||||
|
||||
@@ -18,8 +18,13 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
class ExchangeRateRevaluation(Document):
|
||||
def validate(self):
|
||||
self.validate_rounding_loss_allowance()
|
||||
self.set_total_gain_loss()
|
||||
|
||||
def validate_rounding_loss_allowance(self):
|
||||
if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1):
|
||||
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
|
||||
|
||||
def set_total_gain_loss(self):
|
||||
total_gain_loss = 0
|
||||
|
||||
@@ -92,7 +97,12 @@ class ExchangeRateRevaluation(Document):
|
||||
def get_accounts_data(self):
|
||||
self.validate_mandatory()
|
||||
account_details = self.get_account_balance_from_gle(
|
||||
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
|
||||
company=self.company,
|
||||
posting_date=self.posting_date,
|
||||
account=None,
|
||||
party_type=None,
|
||||
party=None,
|
||||
rounding_loss_allowance=self.rounding_loss_allowance,
|
||||
)
|
||||
accounts_with_new_balance = self.calculate_new_account_balance(
|
||||
self.company, self.posting_date, account_details
|
||||
@@ -104,7 +114,9 @@ class ExchangeRateRevaluation(Document):
|
||||
return accounts_with_new_balance
|
||||
|
||||
@staticmethod
|
||||
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
|
||||
def get_account_balance_from_gle(
|
||||
company, posting_date, account, party_type, party, rounding_loss_allowance
|
||||
):
|
||||
account_details = []
|
||||
|
||||
if company and posting_date:
|
||||
@@ -172,10 +184,18 @@ class ExchangeRateRevaluation(Document):
|
||||
)
|
||||
|
||||
# round off balance based on currency precision
|
||||
# and consider debit-credit difference allowance
|
||||
currency_precision = get_currency_precision()
|
||||
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
|
||||
for acc in account_details:
|
||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
||||
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
|
||||
acc.balance_in_account_currency = 0
|
||||
|
||||
acc.balance = flt(acc.balance, currency_precision)
|
||||
if abs(acc.balance) <= rounding_loss_allowance:
|
||||
acc.balance = 0
|
||||
|
||||
acc.zero_balance = (
|
||||
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
|
||||
)
|
||||
@@ -353,6 +373,24 @@ class ExchangeRateRevaluation(Document):
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(journal_account)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||
"credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
|
||||
# Base currency has balance
|
||||
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
|
||||
@@ -368,22 +406,22 @@ class ExchangeRateRevaluation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(journal_account)
|
||||
journal_entry_accounts.append(journal_account)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
|
||||
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||
"credit": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_total_debit_credit()
|
||||
@@ -531,7 +569,9 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(company, posting_date, account, party_type=None, party=None):
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None
|
||||
):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
@@ -549,7 +589,12 @@ def get_account_details(company, posting_date, account, party_type=None, party=N
|
||||
"account_currency": account_currency,
|
||||
}
|
||||
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
|
||||
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
|
||||
company=company,
|
||||
posting_date=posting_date,
|
||||
account=account,
|
||||
party_type=party_type,
|
||||
party=party,
|
||||
rounding_loss_allowance=rounding_loss_allowance,
|
||||
)
|
||||
|
||||
if account_balance and (
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"fieldname": "current_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Current Exchange Rate",
|
||||
"precision": "9",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -92,6 +93,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "New Exchange Rate",
|
||||
"precision": "9",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -147,7 +149,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-29 19:38:52.915295",
|
||||
"modified": "2023-06-22 12:39:56.446722",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation Account",
|
||||
|
||||
@@ -326,12 +326,10 @@ class JournalEntry(AccountsController):
|
||||
d.db_update()
|
||||
|
||||
def unlink_asset_reference(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset"
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
@@ -358,6 +356,15 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
asset.set_status()
|
||||
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
|
||||
journal_entry_for_scrap = frappe.db.get_value(
|
||||
"Asset", d.reference_name, "journal_entry_for_scrap"
|
||||
)
|
||||
|
||||
if journal_entry_for_scrap == self.name:
|
||||
frappe.throw(
|
||||
_("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.")
|
||||
)
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
@@ -940,6 +947,7 @@ class JournalEntry(AccountsController):
|
||||
blank_row.debit_in_account_currency = abs(diff)
|
||||
blank_row.debit = abs(diff)
|
||||
|
||||
self.set_total_debit_credit()
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -613,7 +613,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
get_outstanding_invoice: function(frm) {
|
||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
const today = frappe.datetime.get_today();
|
||||
const fields = [
|
||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||
@@ -643,12 +643,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
|
||||
let btn_text = "";
|
||||
|
||||
if (get_outstanding_invoices) {
|
||||
btn_text = "Get Outstanding Invoices";
|
||||
}
|
||||
else if (get_orders_to_be_billed) {
|
||||
btn_text = "Get Outstanding Orders";
|
||||
}
|
||||
|
||||
frappe.prompt(fields, function(filters){
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
frm.events.validate_filters_data(frm, filters);
|
||||
frm.doc.cost_center = filters.cost_center;
|
||||
frm.events.get_outstanding_documents(frm, filters);
|
||||
}, __("Filters"), __("Get Outstanding Documents"));
|
||||
frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
|
||||
}, __("Filters"), __(btn_text));
|
||||
},
|
||||
|
||||
get_outstanding_invoices: function(frm) {
|
||||
frm.events.get_outstanding_invoices_or_orders(frm, true, false);
|
||||
},
|
||||
|
||||
get_outstanding_orders: function(frm) {
|
||||
frm.events.get_outstanding_invoices_or_orders(frm, false, true);
|
||||
},
|
||||
|
||||
validate_filters_data: function(frm, filters) {
|
||||
@@ -674,7 +691,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
},
|
||||
|
||||
get_outstanding_documents: function(frm, filters) {
|
||||
get_outstanding_documents: function(frm, filters, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
frm.clear_table("references");
|
||||
|
||||
if(!frm.doc.party) {
|
||||
@@ -698,6 +715,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
args[key] = filters[key];
|
||||
}
|
||||
|
||||
if (get_outstanding_invoices) {
|
||||
args["get_outstanding_invoices"] = true;
|
||||
}
|
||||
else if (get_orders_to_be_billed) {
|
||||
args["get_orders_to_be_billed"] = true;
|
||||
}
|
||||
|
||||
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
|
||||
|
||||
return frappe.call({
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"base_received_amount",
|
||||
"base_received_amount_after_tax",
|
||||
"section_break_14",
|
||||
"get_outstanding_invoice",
|
||||
"get_outstanding_invoices",
|
||||
"get_outstanding_orders",
|
||||
"references",
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
@@ -355,12 +356,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoice",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "references",
|
||||
"fieldtype": "Table",
|
||||
@@ -728,12 +723,24 @@
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoices",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoices"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_orders",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Orders"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:52:30.478523",
|
||||
"modified": "2023-06-19 11:38:04.387219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -148,19 +148,68 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def validate_allocated_amount(self):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
if self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_allocated_amount_with_latest_data()
|
||||
else:
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
for d in self.get("references"):
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
|
||||
for d in self.get("references"):
|
||||
if (flt(d.allocated_amount)) > 0:
|
||||
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(d.reference_doctype, d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and d.outstanding_amount != latest.outstanding_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(d.reference_doctype, d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0:
|
||||
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -373,7 +422,7 @@ class PaymentEntry(AccountsController):
|
||||
for k, v in no_oustanding_refs.items():
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry."
|
||||
"{} - {} now has {} as it had no outstanding amount left before submitting the Payment Entry."
|
||||
).format(
|
||||
_(k),
|
||||
frappe.bold(", ".join(d.reference_name for d in v)),
|
||||
@@ -1309,62 +1358,75 @@ def get_outstanding_reference_documents(args):
|
||||
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"),
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
accounting_dimensions=accounting_dimensions_filter,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
|
||||
for d in outstanding_invoices:
|
||||
d["exchange_rate"] = 1
|
||||
if party_account_currency != company_currency:
|
||||
if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
|
||||
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
|
||||
elif d.voucher_type == "Journal Entry":
|
||||
d["exchange_rate"] = get_exchange_rate(
|
||||
party_account_currency, company_currency, d.posting_date
|
||||
)
|
||||
if d.voucher_type in ("Purchase Invoice"):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get all SO / PO which are not fully billed or against which full advance not paid
|
||||
orders_to_be_billed = []
|
||||
orders_to_be_billed = get_orders_to_be_billed(
|
||||
args.get("posting_date"),
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("company"),
|
||||
party_account_currency,
|
||||
company_currency,
|
||||
filters=args,
|
||||
)
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
outstanding_invoices = []
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
|
||||
if args.get("get_outstanding_invoices"):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
accounting_dimensions=accounting_dimensions_filter,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
|
||||
for d in outstanding_invoices:
|
||||
d["exchange_rate"] = 1
|
||||
if party_account_currency != company_currency:
|
||||
if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
|
||||
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
|
||||
elif d.voucher_type == "Journal Entry":
|
||||
d["exchange_rate"] = get_exchange_rate(
|
||||
party_account_currency, company_currency, d.posting_date
|
||||
)
|
||||
if d.voucher_type in ("Purchase Invoice"):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
party_account_currency,
|
||||
company_currency,
|
||||
condition=condition,
|
||||
)
|
||||
|
||||
# Get all SO / PO which are not fully billed or against which full advance not paid
|
||||
orders_to_be_billed = []
|
||||
if args.get("get_orders_to_be_billed"):
|
||||
orders_to_be_billed = get_orders_to_be_billed(
|
||||
args.get("posting_date"),
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("company"),
|
||||
party_account_currency,
|
||||
company_currency,
|
||||
condition=condition,
|
||||
filters=args,
|
||||
)
|
||||
|
||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||
|
||||
if not data:
|
||||
if args.get("get_outstanding_invoices") and args.get("get_orders_to_be_billed"):
|
||||
ref_document_type = "invoices or orders"
|
||||
elif args.get("get_outstanding_invoices"):
|
||||
ref_document_type = "invoices"
|
||||
elif args.get("get_orders_to_be_billed"):
|
||||
ref_document_type = "orders"
|
||||
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
|
||||
).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
|
||||
"No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
|
||||
).format(
|
||||
ref_document_type, _(args.get("party_type")).lower(), frappe.bold(args.get("party"))
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
@@ -1449,7 +1511,7 @@ def get_orders_to_be_billed(
|
||||
if voucher_type:
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center"):
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
orders = []
|
||||
@@ -1495,9 +1557,15 @@ def get_orders_to_be_billed(
|
||||
|
||||
order_list = []
|
||||
for d in orders:
|
||||
if not (
|
||||
flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
|
||||
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and filters.get("outstanding_amt_less_than")
|
||||
and not (
|
||||
flt(filters.get("outstanding_amt_greater_than"))
|
||||
<= flt(d.outstanding_amount)
|
||||
<= flt(filters.get("outstanding_amt_less_than"))
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
@@ -1013,6 +1013,30 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
|
||||
create_payment_entry(party_type="Employee", party=employee, save=True)
|
||||
|
||||
def test_duplicate_payment_entry_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
def test_duplicate_payment_entry_partial_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.received_amount = si.total / 2
|
||||
pe.references[0].allocated_amount = si.total / 2
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -85,25 +85,29 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
|
||||
if(enabled) {
|
||||
this.frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
|
||||
"args": {
|
||||
for_filter: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party,
|
||||
receivable_payable_account: this.frm.doc.receivable_payable_account
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'is_auto_process_enabled',
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
this.frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
|
||||
"args": {
|
||||
for_filter: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party,
|
||||
receivable_payable_account: this.frm.doc.receivable_payable_account
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
|
||||
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
|
||||
this.frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
});
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
|
||||
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
|
||||
this.frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import frappe
|
||||
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, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -127,12 +126,29 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
ConstantColumn(voucher_type).as_("voucher_type"),
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
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"
|
||||
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||
self.common_filter_conditions.append(ple.account_type == "Receivable")
|
||||
@@ -140,19 +156,10 @@ class PaymentReconciliation(Document):
|
||||
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)
|
||||
)
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
@@ -204,6 +211,15 @@ class PaymentReconciliation(Document):
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
cr_dr_notes = (
|
||||
[x.voucher_no for x in self.return_invoices]
|
||||
if self.party_type in ["Customer", "Supplier"]
|
||||
else []
|
||||
)
|
||||
# Filter out cr/dr notes from outstanding invoices list
|
||||
# Happens when non-standalone cr/dr notes are linked with another invoice through journal entry
|
||||
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
|
||||
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
@@ -236,6 +252,10 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_auto_process_enabled(self):
|
||||
return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments")
|
||||
|
||||
@frappe.whitelist()
|
||||
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
||||
@@ -327,7 +347,10 @@ class PaymentReconciliation(Document):
|
||||
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if payment_details.difference_amount:
|
||||
if payment_details.difference_amount and row.reference_type not in [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
]:
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
@@ -413,6 +436,8 @@ class PaymentReconciliation(Document):
|
||||
journal_entry.save()
|
||||
journal_entry.submit()
|
||||
|
||||
return journal_entry
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -578,6 +603,16 @@ class PaymentReconciliation(Document):
|
||||
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
def get_difference_row(inv):
|
||||
if inv.difference_amount != 0 and inv.difference_account:
|
||||
difference_row = {
|
||||
"account": inv.difference_account,
|
||||
inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
|
||||
reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
}
|
||||
return difference_row
|
||||
|
||||
for inv in dr_cr_notes:
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
@@ -622,5 +657,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if difference_entry := get_difference_row(inv):
|
||||
jv.append("accounts", difference_entry)
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.submit()
|
||||
|
||||
@@ -11,10 +11,13 @@ from frappe.utils import add_days, flt, nowdate
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
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
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
class TestPaymentReconciliation(FrappeTestCase):
|
||||
def setUp(self):
|
||||
@@ -163,7 +166,9 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
def create_payment_reconciliation(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party_type = (
|
||||
self.party_type if hasattr(self, "party_type") and self.party_type else "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()
|
||||
@@ -890,6 +895,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 85)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||
|
||||
def test_reconciliation_purchase_invoice_against_return(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD", currency="USD", conversion_rate=50
|
||||
).submit()
|
||||
|
||||
pi_return = frappe.get_doc(pi.as_dict())
|
||||
pi_return.name = None
|
||||
pi_return.docstatus = 0
|
||||
pi_return.is_return = 1
|
||||
pi_return.conversion_rate = 80
|
||||
pi_return.items[0].qty = -pi_return.items[0].qty
|
||||
pi_return.submit()
|
||||
|
||||
self.company = "_Test Company"
|
||||
self.party_type = "Supplier"
|
||||
self.customer = "_Test Supplier USD"
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = []
|
||||
payments = []
|
||||
for invoice in pr.invoices:
|
||||
if invoice.invoice_number == pi.name:
|
||||
invoices.append(invoice.as_dict())
|
||||
break
|
||||
for payment in pr.payments:
|
||||
if payment.reference_name == pi_return.name:
|
||||
payments.append(payment.as_dict())
|
||||
break
|
||||
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
||||
pr.reconcile()
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Payment Terms Template', {
|
||||
setup: function(frm) {
|
||||
refresh: function(frm) {
|
||||
frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
|
||||
},
|
||||
|
||||
allocate_payment_based_on_payment_terms: function(frm) {
|
||||
frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ from frappe.utils import flt
|
||||
class PaymentTermsTemplate(Document):
|
||||
def validate(self):
|
||||
self.validate_invoice_portion()
|
||||
self.check_duplicate_terms()
|
||||
self.validate_terms()
|
||||
|
||||
def validate_invoice_portion(self):
|
||||
total_portion = 0
|
||||
@@ -23,9 +23,12 @@ class PaymentTermsTemplate(Document):
|
||||
_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
|
||||
)
|
||||
|
||||
def check_duplicate_terms(self):
|
||||
def validate_terms(self):
|
||||
terms = []
|
||||
for term in self.terms:
|
||||
if self.allocate_payment_based_on_payment_terms and not term.payment_term:
|
||||
frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx))
|
||||
|
||||
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
if term_info in terms:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -35,6 +35,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
|
||||
@@ -123,22 +123,29 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
row.expected_amount = row.opening_amount;
|
||||
}
|
||||
|
||||
const pos_inv_promises = frm.doc.pos_transactions.map(
|
||||
row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
|
||||
);
|
||||
|
||||
const pos_invoices = await Promise.all(pos_inv_promises);
|
||||
|
||||
for (let doc of pos_invoices) {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
frappe.call({
|
||||
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
|
||||
args: {
|
||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
||||
pos_profile: frm.doc.pos_profile,
|
||||
user: frm.doc.user
|
||||
},
|
||||
callback: (r) => {
|
||||
let pos_invoices = r.message;
|
||||
for (let doc of pos_invoices) {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
frappe.dom.unfreeze();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="page-break">
|
||||
<div id="header-html" class="hidden-pdf">
|
||||
{% if letter_head %}
|
||||
{% if letter_head.content %}
|
||||
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||
{% endif %}
|
||||
|
||||
@@ -63,6 +63,20 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
||||
frm.set_value('to_date', frappe.datetime.get_today());
|
||||
}
|
||||
},
|
||||
report: function(frm){
|
||||
let filters = {
|
||||
'company': frm.doc.company,
|
||||
}
|
||||
if(frm.doc.report == 'Accounts Receivable'){
|
||||
filters['account_type'] = 'Receivable';
|
||||
}
|
||||
frm.set_query("account", function() {
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
customer_collection: function(frm){
|
||||
frm.set_value('collection_name', '');
|
||||
if(frm.doc.customer_collection){
|
||||
|
||||
@@ -6,17 +6,24 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"report",
|
||||
"section_break_11",
|
||||
"from_date",
|
||||
"posting_date",
|
||||
"company",
|
||||
"account",
|
||||
"group_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
"currency",
|
||||
"project",
|
||||
"payment_terms_template",
|
||||
"sales_partner",
|
||||
"sales_person",
|
||||
"based_on_payment_terms",
|
||||
"section_break_3",
|
||||
"customer_collection",
|
||||
"collection_name",
|
||||
@@ -65,14 +72,14 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_auto_email == 0;",
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Date",
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_auto_email == 0;",
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date",
|
||||
@@ -85,6 +92,7 @@
|
||||
"options": "PSOA Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Project",
|
||||
@@ -102,7 +110,7 @@
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "General Ledger Filters"
|
||||
"label": "Report Filters"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
@@ -162,12 +170,14 @@
|
||||
},
|
||||
{
|
||||
"default": "Group by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "group_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Group By",
|
||||
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
@@ -295,6 +305,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "show_net_values_in_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Net Values in Party Account"
|
||||
@@ -308,10 +319,59 @@
|
||||
{
|
||||
"fieldname": "column_break_ocfq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "report",
|
||||
"fieldtype": "Select",
|
||||
"label": "Report",
|
||||
"options": "General Ledger\nAccounts Receivable",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "payment_terms_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms Template",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "sales_partner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Partner",
|
||||
"options": "Sales Partner"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "sales_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Person",
|
||||
"options": "Sales Person"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "Territory"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "based_on_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Based On Payment Terms"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-04-26 12:46:43.645455",
|
||||
"modified": "2023-06-23 10:13:15.051950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.www.printview import get_print_style
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute as get_ar_soa
|
||||
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import (
|
||||
execute as get_ageing,
|
||||
)
|
||||
@@ -42,29 +43,10 @@ class ProcessStatementOfAccounts(Document):
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = {}
|
||||
ageing = ""
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
)
|
||||
|
||||
for entry in doc.customers:
|
||||
if doc.include_ageing:
|
||||
ageing_filters = frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"report_date": doc.to_date,
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer": entry.customer,
|
||||
}
|
||||
)
|
||||
col1, ageing = get_ageing(ageing_filters)
|
||||
|
||||
if ageing:
|
||||
ageing[0]["ageing_based_on"] = doc.ageing_based_on
|
||||
ageing = set_ageing(doc, entry)
|
||||
|
||||
tax_id = frappe.get_doc("Customer", entry.customer).tax_id
|
||||
presentation_currency = (
|
||||
@@ -72,59 +54,25 @@ def get_report_pdf(doc, consolidated=True):
|
||||
or doc.currency
|
||||
or get_company_currency(doc.company)
|
||||
)
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
filters = get_common_filters(doc)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": doc.from_date,
|
||||
"to_date": doc.to_date,
|
||||
"company": doc.company,
|
||||
"finance_book": doc.finance_book if doc.finance_book else None,
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"currency": doc.currency,
|
||||
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
||||
"project": [p.project_name for p in doc.project],
|
||||
"show_opening_entries": 0,
|
||||
"include_default_book_entries": 0,
|
||||
"tax_id": tax_id if tax_id else None,
|
||||
}
|
||||
)
|
||||
col, res = get_soa(filters)
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
else:
|
||||
filters.update(get_ar_filters(doc, entry))
|
||||
|
||||
for x in [0, -2, -1]:
|
||||
res[x]["account"] = res[x]["account"].replace("'", "")
|
||||
if doc.report == "General Ledger":
|
||||
col, res = get_soa(filters)
|
||||
for x in [0, -2, -1]:
|
||||
res[x]["account"] = res[x]["account"].replace("'", "")
|
||||
if len(res) == 3:
|
||||
continue
|
||||
else:
|
||||
ar_res = get_ar_soa(filters)
|
||||
col, res = ar_res[0], ar_res[1]
|
||||
|
||||
if len(res) == 3:
|
||||
continue
|
||||
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
"filters": filters,
|
||||
"data": res,
|
||||
"ageing": ageing[0] if (doc.include_ageing and ageing) else None,
|
||||
"letter_head": letter_head if doc.letter_head else None,
|
||||
"terms_and_conditions": frappe.db.get_value(
|
||||
"Terms and Conditions", doc.terms_and_conditions, "terms"
|
||||
)
|
||||
if doc.terms_and_conditions
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
html = frappe.render_template(
|
||||
base_template_path,
|
||||
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
|
||||
)
|
||||
statement_dict[entry.customer] = html
|
||||
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
|
||||
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
@@ -137,6 +85,110 @@ def get_report_pdf(doc, consolidated=True):
|
||||
return statement_dict
|
||||
|
||||
|
||||
def set_ageing(doc, entry):
|
||||
ageing_filters = frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"report_date": doc.to_date,
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer": entry.customer,
|
||||
}
|
||||
)
|
||||
col1, ageing = get_ageing(ageing_filters)
|
||||
|
||||
if ageing:
|
||||
ageing[0]["ageing_based_on"] = doc.ageing_based_on
|
||||
|
||||
return ageing
|
||||
|
||||
|
||||
def get_common_filters(doc):
|
||||
return frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"finance_book": doc.finance_book if doc.finance_book else None,
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
return {
|
||||
"from_date": doc.from_date,
|
||||
"to_date": doc.to_date,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"currency": doc.currency,
|
||||
"project": [p.project_name for p in doc.project],
|
||||
"show_opening_entries": 0,
|
||||
"include_default_book_entries": 0,
|
||||
"tax_id": tax_id if tax_id else None,
|
||||
"show_net_values_in_party_account": doc.show_net_values_in_party_account,
|
||||
}
|
||||
|
||||
|
||||
def get_ar_filters(doc, entry):
|
||||
return {
|
||||
"report_date": doc.posting_date if doc.posting_date else None,
|
||||
"customer_name": entry.customer,
|
||||
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
|
||||
"sales_partner": doc.sales_partner if doc.sales_partner else None,
|
||||
"sales_person": doc.sales_person if doc.sales_person else None,
|
||||
"territory": doc.territory if doc.territory else None,
|
||||
"based_on_payment_terms": doc.based_on_payment_terms,
|
||||
"report_name": "Accounts Receivable",
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
|
||||
def get_html(doc, filters, entry, col, res, ageing):
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
if doc.report == "General Ledger"
|
||||
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
)
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
"filters": filters,
|
||||
"data": res,
|
||||
"report": {"report_name": doc.report, "columns": col},
|
||||
"ageing": ageing[0] if (doc.include_ageing and ageing) else None,
|
||||
"letter_head": letter_head if doc.letter_head else None,
|
||||
"terms_and_conditions": frappe.db.get_value(
|
||||
"Terms and Conditions", doc.terms_and_conditions, "terms"
|
||||
)
|
||||
if doc.terms_and_conditions
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
html = frappe.render_template(
|
||||
base_template_path,
|
||||
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
|
||||
fields_dict = {
|
||||
"Customer Group": "customer_group",
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
<style>
|
||||
.print-format {
|
||||
padding: 4mm;
|
||||
font-size: 8.0pt !important;
|
||||
}
|
||||
.print-format td {
|
||||
vertical-align:middle !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h4 class="text-center">
|
||||
{% if (filters.customer_name) %}
|
||||
{{ filters.customer_name }}
|
||||
{% else %}
|
||||
{{ filters.customer ~ filters.supplier }}
|
||||
{% endif %}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
{% if (filters.tax_id) %}
|
||||
{{ _("Tax Id: ") }}{{ filters.tax_id }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h5 class="text-center">
|
||||
{{ _(filters.ageing_based_on) }}
|
||||
{{ _("Until") }}
|
||||
{{ frappe.format(filters.report_date, 'Date') }}
|
||||
</h5>
|
||||
|
||||
<div class="clearfix">
|
||||
<div class="pull-left">
|
||||
{% if(filters.payment_terms) %}
|
||||
<strong>{{ _("Payment Terms") }}:</strong> {{ filters.payment_terms }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if(filters.credit_limit) %}
|
||||
<strong>{{ _("Credit Limit") }}:</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% set balance_row = data.slice(-1).pop() %}
|
||||
{% for i in report.columns %}
|
||||
{% if i.fieldname == 'age' %}
|
||||
{% set elem = i %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set start = report.columns.findIndex(elem) %}
|
||||
{% set range1 = report.columns[start].label %}
|
||||
{% set range2 = report.columns[start+1].label %}
|
||||
{% set range3 = report.columns[start+2].label %}
|
||||
{% set range4 = report.columns[start+3].label %}
|
||||
{% set range5 = report.columns[start+4].label %}
|
||||
{% set range6 = report.columns[start+5].label %}
|
||||
|
||||
{% if(balance_row) %}
|
||||
<table class="table table-bordered table-condensed">
|
||||
<caption class="text-right">(Amount in {{ data[0]["currency"] ~ "" }})</caption>
|
||||
<colgroup>
|
||||
<col style="width: 30mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _(" ") }}</th>
|
||||
<th>{{ _(range1) }}</th>
|
||||
<th>{{ _(range2) }}</th>
|
||||
<th>{{ _(range3) }}</th>
|
||||
<th>{{ _(range4) }}</th>
|
||||
<th>{{ _(range5) }}</th>
|
||||
<th>{{ _(range6) }}</th>
|
||||
<th>{{ _("Total") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ _("Total Outstanding") }}</td>
|
||||
<td class="text-right">
|
||||
{{ format_number(balance_row["age"], null, 2) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
</tr>
|
||||
<td>{{ _("Future Payments") }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<tr class="cvs-footer">
|
||||
<th class="text-left">{{ _("Cheques Required") }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
<th style="width: 10%">{{ _("Date") }}</th>
|
||||
<th style="width: 4%">{{ _("Age (Days)") }}</th>
|
||||
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<th style="width: 14%">{{ _("Reference") }}</th>
|
||||
<th style="width: 10%">{{ _("Sales Person") }}</th>
|
||||
{% else %}
|
||||
<th style="width: 24%">{{ _("Reference") }}</th>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<th style="width: 20%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks") }}
|
||||
{% else %}
|
||||
{{ _("Party") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
|
||||
<th style="width: 10%; text-align: right">
|
||||
{% if report.report_name == "Accounts Receivable" %}
|
||||
{{ _('Credit Note') }}
|
||||
{% else %}
|
||||
{{ _('Debit Note') }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
|
||||
<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
|
||||
<th style="width: 10%">{{ _("Remaining Balance") }}</th>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<th style="width: 40%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks")}}
|
||||
{% else %}
|
||||
{{ _("Party") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
|
||||
<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
|
||||
<th style="width: 15%">
|
||||
{% if report.report_name == "Accounts Receivable Summary" %}
|
||||
{{ _('Credit Note Amount') }}
|
||||
{% else %}
|
||||
{{ _('Debit Note Amount') }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in range(data|length) %}
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
{% if(data[i]["party"]) %}
|
||||
<td>{{ (data[i]["posting_date"]) }}</td>
|
||||
<td style="text-align: right">{{ data[i]["age"] }}</td>
|
||||
<td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
{{ data[i]["voucher_type"] }}
|
||||
<br>
|
||||
{% endif %}
|
||||
{{ data[i]["voucher_no"] }}
|
||||
</td>
|
||||
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<td>{{ data[i]["sales_person"] }}</td>
|
||||
{% endif %}
|
||||
|
||||
{% if not (filters.show_future_payments) %}
|
||||
<td>
|
||||
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
||||
{{ data[i]["party"] }}
|
||||
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["customer_name"] }}
|
||||
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["supplier_name"] }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if data[i]["remarks"] %}
|
||||
{{ _("Remarks") }}:
|
||||
{{ data[i]["remarks"] }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<td style="text-align: right">
|
||||
{{ data[i]["po_no"] }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ _("Total") }}</b></td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
|
||||
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<td style="text-align: right">
|
||||
{{ data[i]["po_no"] }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if(data[i]["party"] or " ") %}
|
||||
{% if not(data[i]["is_total_row"]) %}
|
||||
<td>
|
||||
{% if(not(filters.customer | filters.supplier)) %}
|
||||
{{ data[i]["party"] }}
|
||||
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["customer_name"] }}
|
||||
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["supplier_name"] }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>{{ _("Remarks") }}:
|
||||
{{ data[i]["remarks"] }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td><b>{{ _("Total") }}</b></td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{% if ageing %}
|
||||
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
|
||||
{{ _("up to " ) }} {{ frappe.format(filters.report_date, 'Date')}}
|
||||
</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">30 Days</th>
|
||||
<th style="width: 25%">60 Days</th>
|
||||
<th style="width: 25%">90 Days</th>
|
||||
<th style="width: 25%">120 Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
|
||||
@@ -637,13 +637,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
gle_filters={"account": "Stock In Hand - TCP1"},
|
||||
)
|
||||
|
||||
# assert loss booked in COGS
|
||||
self.assertGLEs(
|
||||
return_pi,
|
||||
[{"credit": 0, "debit": 200}],
|
||||
gle_filters={"account": "Cost of Goods Sold - TCP1"},
|
||||
)
|
||||
|
||||
def test_return_with_lcv(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
@@ -1662,6 +1655,21 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
def test_gl_entries_for_standalone_debit_note(self):
|
||||
make_purchase_invoice(qty=5, rate=500, update_stock=True)
|
||||
|
||||
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
|
||||
|
||||
# override the rate with valuation rate
|
||||
sle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["stock_value_difference", "actual_qty"],
|
||||
filters={"voucher_no": returned_inv.name},
|
||||
)[0]
|
||||
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -320,6 +320,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_debit_note",
|
||||
"fieldname": "is_return",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -1959,6 +1960,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_return",
|
||||
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
|
||||
"fieldname": "is_debit_note",
|
||||
"fieldtype": "Check",
|
||||
@@ -2153,7 +2155,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-04-28 14:15:59.901154",
|
||||
"modified": "2023-06-19 16:02:05.309332",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -1012,10 +1012,16 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def check_prev_docstatus(self):
|
||||
for d in self.get("items"):
|
||||
if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1:
|
||||
if (
|
||||
d.sales_order
|
||||
and frappe.db.get_value("Sales Order", d.sales_order, "docstatus", cache=True) != 1
|
||||
):
|
||||
frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order))
|
||||
|
||||
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
|
||||
if (
|
||||
d.delivery_note
|
||||
and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus", cache=True) != 1
|
||||
):
|
||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2017-12-25 16:50:53.878430",
|
||||
"doctype": "DocType",
|
||||
@@ -111,11 +112,12 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"modified": "2019-11-17 23:24:11.395882",
|
||||
"links": [],
|
||||
"modified": "2023-04-10 22:02:20.406087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Shareholder",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -158,6 +160,7 @@
|
||||
"search_fields": "folio_no",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, getdate
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
@@ -569,7 +569,14 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
|
||||
{
|
||||
"supplier": ("in", parties),
|
||||
"apply_tds": 1,
|
||||
"docstatus": 1,
|
||||
"tax_withholding_category": ldc.tax_withholding_category,
|
||||
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
|
||||
"company": ldc.company,
|
||||
},
|
||||
"sum(tax_withholding_net_total)",
|
||||
)
|
||||
|
||||
@@ -584,10 +591,10 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if current_amount < (certificate_limit - deducted_amount):
|
||||
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
|
||||
return current_amount * rate / 100
|
||||
else:
|
||||
ltds_amount = certificate_limit - deducted_amount
|
||||
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||
tds_amount = current_amount - ltds_amount
|
||||
|
||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||
@@ -598,9 +605,9 @@ def is_valid_certificate(
|
||||
):
|
||||
valid = False
|
||||
|
||||
if (
|
||||
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
|
||||
) and certificate_limit > deducted_amount:
|
||||
available_amount = flt(certificate_limit) - flt(deducted_amount)
|
||||
|
||||
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
@@ -850,7 +852,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
return company_wise_info
|
||||
|
||||
|
||||
def get_party_shipping_address(doctype, name):
|
||||
def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
|
||||
and/or `is_shipping_address = 1`.
|
||||
@@ -861,22 +863,23 @@ def get_party_shipping_address(doctype, name):
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"SELECT dl.parent "
|
||||
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name "
|
||||
"where "
|
||||
"dl.link_doctype=%s "
|
||||
"and dl.link_name=%s "
|
||||
"and dl.parenttype='Address' "
|
||||
"and ifnull(ta.disabled, 0) = 0 and"
|
||||
"(ta.address_type='Shipping' or ta.is_shipping_address=1) "
|
||||
"order by ta.is_shipping_address desc, ta.address_type desc limit 1",
|
||||
(doctype, name),
|
||||
shipping_addresses = frappe.get_all(
|
||||
"Address",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
["disabled", "=", 0],
|
||||
],
|
||||
or_filters=[
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
if out:
|
||||
return out[0][0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
@@ -910,31 +913,32 @@ def get_partywise_advanced_payment_amount(
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
def get_default_contact(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns default contact for the given doctype and name.
|
||||
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
|
||||
Returns contact name only if there is a primary contact for given doctype and name.
|
||||
|
||||
Else returns None
|
||||
|
||||
:param doctype: Party Doctype
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"""
|
||||
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
|
||||
FROM `tabDynamic Link` dl
|
||||
INNER JOIN `tabContact` c ON c.name = dl.parent
|
||||
WHERE
|
||||
dl.link_doctype=%s AND
|
||||
dl.link_name=%s AND
|
||||
dl.parenttype = 'Contact'
|
||||
ORDER BY is_primary_contact DESC, is_billing_contact DESC
|
||||
""",
|
||||
(doctype, name),
|
||||
contacts = frappe.get_all(
|
||||
"Contact",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
],
|
||||
or_filters=[
|
||||
["is_primary_contact", "=", 1],
|
||||
["is_billing_contact", "=", 1],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_primary_contact DESC, is_billing_contact DESC",
|
||||
)
|
||||
if out:
|
||||
try:
|
||||
return out[0][0]
|
||||
except Exception:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return contacts[0] if contacts else None
|
||||
|
||||
|
||||
def add_party_account(party_type, party, company, account):
|
||||
|
||||
@@ -284,4 +284,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
@@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# 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 ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
@@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1}
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
|
||||
@@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_payment_against_credit_note(self):
|
||||
"""
|
||||
Payment against credit/debit note should be considered against the parent invoice
|
||||
"""
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
si1 = make_sales_invoice()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
cr_note = make_credit_note(si1.name)
|
||||
|
||||
si2 = make_sales_invoice()
|
||||
|
||||
# manually link cr_note with si2 using journal entry
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = company
|
||||
je.voucher_type = "Credit Note"
|
||||
je.posting_date = today()
|
||||
|
||||
debit_account = "Debtors - _TC2"
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"debit": 100,
|
||||
"debit_in_account_currency": 100,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
credit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"credit": 100,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si2.doctype,
|
||||
"reference_name": si2.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
je.append("accounts", credit_entry)
|
||||
je = je.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
@@ -256,7 +317,7 @@ def make_payment(docname):
|
||||
|
||||
|
||||
def make_credit_note(docname):
|
||||
create_sales_invoice(
|
||||
credit_note = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
@@ -269,3 +330,5 @@ def make_credit_note(docname):
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
|
||||
@@ -15,7 +15,6 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i
|
||||
get_group_by_conditions,
|
||||
get_tax_accounts,
|
||||
)
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -40,6 +39,16 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
tax_doctype="Purchase Taxes and Charges",
|
||||
)
|
||||
|
||||
scrubbed_tax_fields = {}
|
||||
|
||||
for tax in tax_columns:
|
||||
scrubbed_tax_fields.update(
|
||||
{
|
||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
||||
}
|
||||
)
|
||||
|
||||
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||
|
||||
data = []
|
||||
@@ -50,11 +59,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
if filters.get("group_by"):
|
||||
grand_total = get_grand_total(filters, "Purchase Invoice")
|
||||
|
||||
item_details = get_item_details()
|
||||
|
||||
for d in item_list:
|
||||
item_record = item_details.get(d.item_code)
|
||||
|
||||
purchase_receipt = None
|
||||
if d.purchase_receipt:
|
||||
purchase_receipt = d.purchase_receipt
|
||||
@@ -67,8 +72,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
|
||||
row = {
|
||||
"item_code": d.item_code,
|
||||
"item_name": item_record.item_name if item_record else d.item_name,
|
||||
"item_group": item_record.item_group if item_record else d.item_group,
|
||||
"item_name": d.pi_item_name if d.pi_item_name else d.i_item_name,
|
||||
"item_group": d.pi_item_group if d.pi_item_group else d.i_item_group,
|
||||
"description": d.description,
|
||||
"invoice": d.parent,
|
||||
"posting_date": d.posting_date,
|
||||
@@ -87,7 +92,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
"project": d.project,
|
||||
"company": d.company,
|
||||
"purchase_order": d.purchase_order,
|
||||
"purchase_receipt": d.purchase_receipt,
|
||||
"purchase_receipt": purchase_receipt,
|
||||
"expense_account": expense_account,
|
||||
"stock_qty": d.stock_qty,
|
||||
"stock_uom": d.stock_uom,
|
||||
@@ -101,8 +106,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
{
|
||||
frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0),
|
||||
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
|
||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
@@ -241,7 +246,7 @@ def get_columns(additional_table_columns, filters):
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Receipt"),
|
||||
"fieldname": "Purchase Receipt",
|
||||
"fieldname": "purchase_receipt",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Receipt",
|
||||
"width": 100,
|
||||
@@ -325,15 +330,17 @@ def get_items(filters, additional_query_columns):
|
||||
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
|
||||
`tabPurchase Invoice`.unrealized_profit_loss_account,
|
||||
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
|
||||
`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
|
||||
`tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
|
||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
||||
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
|
||||
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
|
||||
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
|
||||
`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
|
||||
`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {0}
|
||||
from `tabPurchase Invoice`, `tabPurchase Invoice Item`
|
||||
from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
|
||||
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
|
||||
`tabPurchase Invoice`.docstatus = 1 %s
|
||||
`tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
|
||||
`tabPurchase Invoice`.docstatus = 1 %s
|
||||
""".format(
|
||||
additional_query_columns
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ from frappe.utils.xlsxutils import handle_html
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
|
||||
get_customer_details,
|
||||
get_item_details,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,6 +34,16 @@ def _execute(
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
scrubbed_tax_fields = {}
|
||||
|
||||
for tax in tax_columns:
|
||||
scrubbed_tax_fields.update(
|
||||
{
|
||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
||||
}
|
||||
)
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
|
||||
@@ -47,11 +56,9 @@ def _execute(
|
||||
grand_total = get_grand_total(filters, "Sales Invoice")
|
||||
|
||||
customer_details = get_customer_details()
|
||||
item_details = get_item_details()
|
||||
|
||||
for d in item_list:
|
||||
customer_record = customer_details.get(d.customer)
|
||||
item_record = item_details.get(d.item_code)
|
||||
|
||||
delivery_note = None
|
||||
if d.delivery_note:
|
||||
@@ -64,8 +71,8 @@ def _execute(
|
||||
|
||||
row = {
|
||||
"item_code": d.item_code,
|
||||
"item_name": item_record.item_name if item_record else d.item_name,
|
||||
"item_group": item_record.item_group if item_record else d.item_group,
|
||||
"item_name": d.si_item_name if d.si_item_name else d.i_item_name,
|
||||
"item_group": d.si_item_group if d.si_item_group else d.i_item_group,
|
||||
"description": d.description,
|
||||
"invoice": d.parent,
|
||||
"posting_date": d.posting_date,
|
||||
@@ -107,8 +114,8 @@ def _execute(
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
{
|
||||
frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0),
|
||||
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
|
||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
if item_tax.get("is_other_charges"):
|
||||
@@ -399,19 +406,23 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
|
||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
|
||||
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
|
||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
|
||||
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0}
|
||||
from `tabSales Invoice`, `tabSales Invoice Item`
|
||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
and `tabSales Invoice`.docstatus = 1 {1}
|
||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
|
||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
|
||||
`tabItem`.name = `tabSales Invoice Item`.`item_code` and
|
||||
`tabSales Invoice`.docstatus = 1 {1}
|
||||
""".format(
|
||||
additional_query_columns or "", conditions
|
||||
),
|
||||
|
||||
@@ -221,11 +221,6 @@ def get_balance_on(
|
||||
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
||||
acc.check_permission("read")
|
||||
|
||||
if report_type == "Profit and Loss":
|
||||
# for pl accounts, get balance within a fiscal year
|
||||
cond.append(
|
||||
"posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date
|
||||
)
|
||||
# different filter for group and ledger - improved performance
|
||||
if acc.is_group:
|
||||
cond.append(
|
||||
|
||||
@@ -26,6 +26,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
is_first_day_of_the_month,
|
||||
is_last_day_of_the_month,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
@@ -359,8 +360,14 @@ class Asset(AccountsController):
|
||||
break
|
||||
|
||||
# For first row
|
||||
if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation:
|
||||
from_date = add_days(self.available_for_use_date, -1)
|
||||
if (
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
self.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
@@ -369,10 +376,18 @@ class Asset(AccountsController):
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
from_date = add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
if not is_first_day_of_the_month(getdate(self.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
((self.number_of_depreciations_booked - 1) * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
else:
|
||||
from_date = add_months(
|
||||
getdate(add_days(self.available_for_use_date, -1)),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_months, cint, flt, get_last_day, getdate, nowdate, today
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
nowdate,
|
||||
today,
|
||||
)
|
||||
from frappe.utils.data import get_link_to_form
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
@@ -128,15 +137,15 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
|
||||
d.db_set("journal_entry", je.name)
|
||||
|
||||
idx = cint(d.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(d.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
|
||||
@@ -334,6 +343,9 @@ def modify_depreciation_schedule_for_asset_repairs(asset):
|
||||
|
||||
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
if not asset.calculate_depreciation:
|
||||
return
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
@@ -474,18 +486,22 @@ def get_gl_entries_on_asset_disposal(
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
]
|
||||
|
||||
if accumulated_depr_amount:
|
||||
gl_entries.append(
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
)
|
||||
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
@@ -591,3 +607,9 @@ 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)
|
||||
|
||||
|
||||
def is_first_day_of_the_month(date):
|
||||
first_day_of_the_month = get_first_day(date)
|
||||
|
||||
return getdate(first_day_of_the_month) == getdate(date)
|
||||
|
||||
@@ -766,14 +766,14 @@ class TestDepreciationMethods(AssetSetup):
|
||||
number_of_depreciations_booked=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2030-12-31",
|
||||
depreciation_start_date="2031-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=12,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
|
||||
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]]
|
||||
expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
|
||||
@@ -14,7 +14,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
}
|
||||
|
||||
refresh() {
|
||||
erpnext.hide_company();
|
||||
this.show_general_ledger();
|
||||
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
|
||||
this.show_stock_ledger();
|
||||
@@ -105,10 +104,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
return this.get_target_item_details();
|
||||
}
|
||||
|
||||
target_asset() {
|
||||
return this.get_target_asset_details();
|
||||
}
|
||||
|
||||
item_code(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Stock Item") {
|
||||
@@ -223,26 +218,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
}
|
||||
}
|
||||
|
||||
get_target_asset_details() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.target_asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
asset: me.frm.doc.target_asset,
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_consumed_stock_item_details(row) {
|
||||
var me = this;
|
||||
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
"naming_series",
|
||||
"entry_type",
|
||||
"target_item_code",
|
||||
"target_asset",
|
||||
"target_item_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"column_break_9",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_asset_location",
|
||||
"target_warehouse",
|
||||
"target_qty",
|
||||
"target_stock_uom",
|
||||
@@ -85,14 +86,13 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset"
|
||||
"options": "Asset",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
@@ -108,11 +108,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -158,7 +158,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Stock Items"
|
||||
@@ -189,7 +189,7 @@
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty",
|
||||
"read_only_depends_on": "target_is_fixed_asset"
|
||||
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
|
||||
},
|
||||
{
|
||||
"fetch_from": "target_item_code.stock_uom",
|
||||
@@ -227,7 +227,7 @@
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
|
||||
"fieldname": "section_break_26",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Asset Items"
|
||||
"label": "Consumed Assets"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_items",
|
||||
@@ -266,7 +266,7 @@
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))",
|
||||
"fieldname": "service_expenses_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Service Expenses"
|
||||
@@ -329,12 +329,20 @@
|
||||
"label": "Target Fixed Asset Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "target_asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Asset Location",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"options": "Location"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-12 15:09:40.771332",
|
||||
"modified": "2023-06-22 14:17:07.995120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
|
||||
# import erpnext
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
@@ -43,7 +43,6 @@ force_fields = [
|
||||
"target_has_batch_no",
|
||||
"target_stock_uom",
|
||||
"stock_uom",
|
||||
"target_fixed_asset_account",
|
||||
"fixed_asset_account",
|
||||
"valuation_rate",
|
||||
]
|
||||
@@ -54,7 +53,6 @@ class AssetCapitalization(StockController):
|
||||
self.validate_posting_time()
|
||||
self.set_missing_values(for_validate=True)
|
||||
self.validate_target_item()
|
||||
self.validate_target_asset()
|
||||
self.validate_consumed_stock_item()
|
||||
self.validate_consumed_asset_item()
|
||||
self.validate_service_item()
|
||||
@@ -65,17 +63,18 @@ class AssetCapitalization(StockController):
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_source_mandatory()
|
||||
if self.entry_type == "Capitalization":
|
||||
self.create_target_asset()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
@@ -86,15 +85,6 @@ class AssetCapitalization(StockController):
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
# Remove asset if item not a fixed asset
|
||||
if not self.target_is_fixed_asset:
|
||||
self.target_asset = None
|
||||
|
||||
target_asset_details = get_target_asset_details(self.target_asset, self.company)
|
||||
for k, v in target_asset_details.items():
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
for d in self.stock_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
@@ -146,9 +136,6 @@ class AssetCapitalization(StockController):
|
||||
|
||||
if not target_item.is_stock_item:
|
||||
self.target_warehouse = None
|
||||
if not target_item.is_fixed_asset:
|
||||
self.target_asset = None
|
||||
self.target_fixed_asset_account = None
|
||||
if not target_item.has_batch_no:
|
||||
self.target_batch_no = None
|
||||
if not target_item.has_serial_no:
|
||||
@@ -159,17 +146,6 @@ class AssetCapitalization(StockController):
|
||||
|
||||
self.validate_item(target_item)
|
||||
|
||||
def validate_target_asset(self):
|
||||
if self.target_asset:
|
||||
target_asset = self.get_asset_for_validation(self.target_asset)
|
||||
|
||||
if target_asset.item_code != self.target_item_code:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
|
||||
)
|
||||
|
||||
self.validate_asset(target_asset)
|
||||
|
||||
def validate_consumed_stock_item(self):
|
||||
for d in self.stock_items:
|
||||
if d.item_code:
|
||||
@@ -379,7 +355,11 @@ class AssetCapitalization(StockController):
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
|
||||
if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable:
|
||||
return []
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
@@ -422,11 +402,14 @@ class AssetCapitalization(StockController):
|
||||
def get_gl_entries_for_consumed_asset_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
self.are_all_asset_items_non_depreciable = True
|
||||
|
||||
# Consumed Assets
|
||||
for item in self.asset_items:
|
||||
asset = self.get_asset(item)
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.are_all_asset_items_non_depreciable = False
|
||||
depreciate_asset(asset, self.posting_date)
|
||||
asset.reload()
|
||||
|
||||
@@ -507,30 +490,41 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
def create_target_asset(self):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
if self.docstatus == 1 and self.entry_type == "Capitalization":
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.prepare_depreciation_data()
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
asset_doc.save()
|
||||
elif self.docstatus == 2:
|
||||
for item in self.asset_items:
|
||||
asset = self.get_asset(item)
|
||||
asset.db_set("disposal_date", None)
|
||||
self.set_consumed_asset_status(asset)
|
||||
asset_doc = frappe.new_doc("Asset")
|
||||
asset_doc.company = self.company
|
||||
asset_doc.item_code = self.target_item_code
|
||||
asset_doc.is_existing_asset = 1
|
||||
asset_doc.location = self.target_asset_location
|
||||
asset_doc.available_for_use_date = self.posting_date
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.insert()
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
self.target_asset = asset_doc.name
|
||||
|
||||
def get_asset(self, item):
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
self.check_finance_books(item, asset)
|
||||
return asset
|
||||
self.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Asset {0} has been created. Please set the depreciation details if any and submit it."
|
||||
).format(get_link_to_form("Asset", asset_doc.name))
|
||||
)
|
||||
|
||||
def restore_consumed_asset_items(self):
|
||||
for item in self.asset_items:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
asset.db_set("disposal_date", None)
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
|
||||
def set_consumed_asset_status(self, asset):
|
||||
if self.docstatus == 1:
|
||||
@@ -580,33 +574,6 @@ def get_target_item_details(item_code=None, company=None):
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_asset_details(asset=None, company=None):
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Asset Details
|
||||
asset_details = frappe._dict()
|
||||
if asset:
|
||||
asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
|
||||
if not asset_details:
|
||||
frappe.throw(_("Asset {0} does not exist").format(asset))
|
||||
|
||||
# Re-set item code from Asset
|
||||
out.target_item_code = asset_details.item_code
|
||||
|
||||
# Set Asset Details
|
||||
out.asset_name = asset_details.asset_name
|
||||
|
||||
if asset_details.item_code:
|
||||
out.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=asset_details.item_code, company=company
|
||||
)
|
||||
else:
|
||||
out.target_fixed_asset_account = None
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_stock_item_details(args):
|
||||
if isinstance(args, string_types):
|
||||
|
||||
@@ -39,13 +39,6 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
|
||||
total_amount = 103000
|
||||
|
||||
# Create assets
|
||||
target_asset = create_asset(
|
||||
asset_name="Asset Capitalization Target Asset",
|
||||
submit=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
consumed_asset = create_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_value,
|
||||
@@ -57,7 +50,8 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
target_asset=target_asset.name,
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
consumed_asset=consumed_asset.name,
|
||||
@@ -86,7 +80,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset.reload()
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
|
||||
@@ -134,13 +128,6 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
|
||||
total_amount = 103000
|
||||
|
||||
# Create assets
|
||||
target_asset = create_asset(
|
||||
asset_name="Asset Capitalization Target Asset",
|
||||
submit=1,
|
||||
warehouse="Stores - _TC",
|
||||
company=company,
|
||||
)
|
||||
consumed_asset = create_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_value,
|
||||
@@ -152,7 +139,8 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
target_asset=target_asset.name,
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
consumed_asset=consumed_asset.name,
|
||||
@@ -181,7 +169,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset.reload()
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
|
||||
@@ -343,6 +331,7 @@ def create_asset_capitalization(**args):
|
||||
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
|
||||
"target_item_code": target_item_code,
|
||||
"target_asset": target_asset.name,
|
||||
"target_asset_location": "Test Location",
|
||||
"target_warehouse": target_warehouse,
|
||||
"target_qty": flt(args.target_qty) or 1,
|
||||
"target_batch_no": args.target_batch_no,
|
||||
|
||||
@@ -70,19 +70,21 @@ frappe.ui.form.on('Asset Movement', {
|
||||
else if (frm.doc.purpose === 'Issue') {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 1, reqd: 0 },
|
||||
source_location: { read_only: 1, reqd: 1 },
|
||||
source_location: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 1 }
|
||||
};
|
||||
}
|
||||
Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
|
||||
let property_to_be_altered = fieldnames_to_be_altered[fieldname];
|
||||
Object.keys(property_to_be_altered).forEach(property => {
|
||||
let value = property_to_be_altered[property];
|
||||
frm.set_df_property(fieldname, property, value, cdn, 'assets');
|
||||
if (fieldnames_to_be_altered) {
|
||||
Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
|
||||
let property_to_be_altered = fieldnames_to_be_altered[fieldname];
|
||||
Object.keys(property_to_be_altered).forEach(property => {
|
||||
let value = property_to_be_altered[property];
|
||||
frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
frm.refresh_field('assets');
|
||||
frm.refresh_field('assets');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
@@ -95,10 +96,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-22 12:30:55.295670",
|
||||
"modified": "2023-06-28 16:54:26.571083",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Movement",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -148,5 +150,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -28,25 +28,20 @@ class AssetMovement(Document):
|
||||
def validate_location(self):
|
||||
for d in self.assets:
|
||||
if self.purpose in ["Transfer", "Issue"]:
|
||||
if not d.source_location:
|
||||
d.source_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
|
||||
if not d.source_location:
|
||||
frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset))
|
||||
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
|
||||
if self.purpose == "Issue":
|
||||
if d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Issuing cannot be done to a location. Please enter employee who has issued Asset {0}"
|
||||
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
@@ -107,12 +102,12 @@ class AssetMovement(Document):
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.set_latest_location_in_asset()
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_latest_location_in_asset()
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def set_latest_location_in_asset(self):
|
||||
def set_latest_location_and_custodian_in_asset(self):
|
||||
current_location, current_employee = "", ""
|
||||
cond = "1=1"
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
if not frappe.db.exists("Location", "Test Location 2"):
|
||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
||||
|
||||
movement1 = create_asset_movement(
|
||||
create_asset_movement(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
assets=[
|
||||
@@ -58,7 +58,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
|
||||
create_asset_movement(
|
||||
movement1 = create_asset_movement(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
assets=[
|
||||
@@ -70,21 +70,32 @@ class TestAssetMovement(unittest.TestCase):
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
movement1.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
|
||||
employee = make_employee("testassetmovemp@example.com", company="_Test Company")
|
||||
create_asset_movement(
|
||||
purpose="Issue",
|
||||
company=asset.company,
|
||||
assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}],
|
||||
assets=[{"asset": asset.name, "source_location": "Test Location 2", "to_employee": employee}],
|
||||
reference_doctype="Purchase Receipt",
|
||||
reference_name=pr.name,
|
||||
)
|
||||
|
||||
# after issuing asset should belong to an employee not at a location
|
||||
# after issuing, asset should belong to an employee not at a location
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
|
||||
|
||||
create_asset_movement(
|
||||
purpose="Receipt",
|
||||
company=asset.company,
|
||||
assets=[{"asset": asset.name, "from_employee": employee, "target_location": "Test Location"}],
|
||||
reference_doctype="Purchase Receipt",
|
||||
reference_name=pr.name,
|
||||
)
|
||||
|
||||
# after receiving, asset should belong to a location not at an employee
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
def test_last_movement_cancellation(self):
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
|
||||
|
||||
@@ -19,56 +19,6 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
options: "\nIn Location\nDisposed",
|
||||
default: 'In Location'
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.nowdate(),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"date_based_on",
|
||||
"label": __("Date Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Purchase Date", "Available For Use Date"],
|
||||
"default": "Purchase Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
fieldname:"asset_category",
|
||||
label: __("Asset Category"),
|
||||
@@ -89,22 +39,67 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
default: "--Select a group--",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
depends_on: "eval: doc.filter_by_finance_book == 1",
|
||||
},
|
||||
{
|
||||
fieldname:"filter_by_finance_book",
|
||||
label: __("Filter by Finance Book"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
{
|
||||
fieldname:"only_existing_assets",
|
||||
label: __("Only existing assets"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
{
|
||||
"fieldname": "include_default_book_assets",
|
||||
"label": __("Include Default Book Assets"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["--Select a period--", "Fiscal Year", "Date Range"],
|
||||
"default": "--Select a period--",
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.nowdate(),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||
},
|
||||
{
|
||||
"fieldname":"date_based_on",
|
||||
"label": __("Date Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Purchase Date", "Available For Use Date"],
|
||||
"default": "Purchase Date",
|
||||
"depends_on": "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'",
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import cstr, flt, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
@@ -13,7 +15,6 @@ from erpnext.accounts.report.financial_statements import (
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -64,11 +65,9 @@ def get_conditions(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
data = []
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
depreciation_amount_map = get_finance_book_value_map(filters)
|
||||
pr_supplier_map = get_purchase_receipt_supplier_map()
|
||||
pi_supplier_map = get_purchase_invoice_supplier_map()
|
||||
|
||||
@@ -102,20 +101,31 @@ def get_data(filters):
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
assets_linked_to_fb = None
|
||||
assets_linked_to_fb = get_assets_linked_to_fb(filters)
|
||||
|
||||
if filters.filter_by_finance_book:
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.include_default_book_assets and company_fb:
|
||||
finance_book = company_fb
|
||||
elif filters.finance_book:
|
||||
finance_book = filters.finance_book
|
||||
else:
|
||||
finance_book = None
|
||||
|
||||
depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book)
|
||||
|
||||
for asset in assets_record:
|
||||
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
||||
if (
|
||||
assets_linked_to_fb
|
||||
and asset.calculate_depreciation
|
||||
and asset.asset_id not in assets_linked_to_fb
|
||||
):
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
||||
asset_value = get_asset_value_after_depreciation(
|
||||
asset.asset_id, finance_book
|
||||
) or get_asset_value_after_depreciation(asset.asset_id)
|
||||
|
||||
row = {
|
||||
"asset_id": asset.asset_id,
|
||||
"asset_name": asset.asset_name,
|
||||
@@ -126,7 +136,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters),
|
||||
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map),
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
@@ -140,14 +150,23 @@ def get_data(filters):
|
||||
|
||||
def prepare_chart_data(data, filters):
|
||||
labels_values_map = {}
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||
filters_filter_based_on = "Date Range"
|
||||
date_field = "purchase_date"
|
||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
else:
|
||||
filters_filter_based_on = filters.filter_based_on
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
filters_from_date = filters.from_date
|
||||
filters_to_date = filters.to_date
|
||||
|
||||
period_list = get_period_list(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
filters.filter_based_on,
|
||||
filters_from_date,
|
||||
filters_to_date,
|
||||
filters_filter_based_on,
|
||||
"Monthly",
|
||||
company=filters.company,
|
||||
ignore_fiscal_year=True,
|
||||
@@ -184,57 +203,76 @@ def prepare_chart_data(data, filters):
|
||||
}
|
||||
|
||||
|
||||
def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
|
||||
if asset.calculate_depreciation:
|
||||
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
else:
|
||||
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
|
||||
def get_assets_linked_to_fb(filters):
|
||||
afb = frappe.qb.DocType("Asset Finance Book")
|
||||
|
||||
return flt(depr_amount, 2)
|
||||
|
||||
|
||||
def get_finance_book_value_map(filters):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
return frappe._dict(
|
||||
frappe.db.sql(
|
||||
""" Select
|
||||
parent, SUM(depreciation_amount)
|
||||
FROM `tabDepreciation Schedule`
|
||||
WHERE
|
||||
parentfield='schedules'
|
||||
AND schedule_date<=%s
|
||||
AND journal_entry IS NOT NULL
|
||||
AND ifnull(finance_book, '')=%s
|
||||
GROUP BY parent""",
|
||||
(date, cstr(filters.finance_book or "")),
|
||||
)
|
||||
query = frappe.qb.from_(afb).select(
|
||||
afb.parent,
|
||||
)
|
||||
|
||||
if filters.include_default_book_assets:
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
def get_manual_depreciation_amount_of_asset(asset, filters):
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'"))
|
||||
|
||||
query = query.where(
|
||||
(afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (afb.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
(afb.finance_book.isin([cstr(filters.finance_book), ""])) | (afb.finance_book.isnull())
|
||||
)
|
||||
|
||||
assets_linked_to_fb = list(chain(*query.run(as_list=1)))
|
||||
|
||||
return assets_linked_to_fb
|
||||
|
||||
|
||||
def get_depreciation_amount_of_asset(asset, depreciation_amount_map):
|
||||
return depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
|
||||
|
||||
def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
|
||||
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
aca = frappe.qb.DocType("Asset Category Account")
|
||||
company = frappe.qb.DocType("Company")
|
||||
|
||||
result = (
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.debit))
|
||||
.where(gle.against_voucher == asset.asset_id)
|
||||
.where(gle.account == depreciation_expense_account)
|
||||
.join(asset)
|
||||
.on(gle.against_voucher == asset.name)
|
||||
.join(aca)
|
||||
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
|
||||
.join(company)
|
||||
.on(company.name == asset.company)
|
||||
.select(asset.name.as_("asset"), Sum(gle.debit).as_("depreciation_amount"))
|
||||
.where(
|
||||
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.posting_date <= date)
|
||||
).run()
|
||||
.where(asset.docstatus == 1)
|
||||
.groupby(asset.name)
|
||||
)
|
||||
|
||||
if result and result[0] and result[0][0]:
|
||||
depr_amount = result[0][0]
|
||||
if finance_book:
|
||||
query = query.where(
|
||||
(gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
depr_amount = 0
|
||||
query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull()))
|
||||
|
||||
return depr_amount
|
||||
if filters.filter_based_on in ("Date Range", "Fiscal Year"):
|
||||
query = query.where(gle.posting_date <= date)
|
||||
|
||||
asset_depr_amount_map = query.run()
|
||||
|
||||
return dict(asset_depr_amount_map)
|
||||
|
||||
|
||||
def get_purchase_receipt_supplier_map():
|
||||
|
||||
@@ -286,7 +286,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
source_name: this.frm.doc.supplier,
|
||||
target: this.frm,
|
||||
setters: {
|
||||
company: me.frm.doc.company
|
||||
company: this.frm.doc.company
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: ["!=", 2],
|
||||
|
||||
@@ -457,7 +457,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2022-11-09 18:02:59.075203",
|
||||
"modified": "2023-05-09 15:34:13.408932",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
||||
@@ -917,6 +917,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
return is_inclusive
|
||||
|
||||
def should_show_taxes_as_table_in_print(self):
|
||||
return cint(frappe.db.get_single_value("Accounts Settings", "show_taxes_as_table_in_print"))
|
||||
|
||||
def validate_advance_entries(self):
|
||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
@@ -30,6 +30,8 @@ class BuyingController(SubcontractingController):
|
||||
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
|
||||
|
||||
def validate(self):
|
||||
self.set_rate_for_standalone_debit_note()
|
||||
|
||||
super(BuyingController, self).validate()
|
||||
if getattr(self, "supplier", None) and not self.supplier_name:
|
||||
self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name")
|
||||
@@ -72,6 +74,30 @@ class BuyingController(SubcontractingController):
|
||||
),
|
||||
)
|
||||
|
||||
def set_rate_for_standalone_debit_note(self):
|
||||
if self.get("is_return") and self.get("update_stock") and not self.return_against:
|
||||
for row in self.items:
|
||||
|
||||
# override the rate with valuation rate
|
||||
row.rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"posting_date": self.get("posting_date"),
|
||||
"posting_time": self.get("posting_time"),
|
||||
"qty": row.qty,
|
||||
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
row.discount_percentage = 0.0
|
||||
row.discount_amount = 0.0
|
||||
row.margin_rate_or_amount = 0.0
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
|
||||
@@ -445,7 +471,7 @@ class BuyingController(SubcontractingController):
|
||||
continue
|
||||
|
||||
if d.warehouse:
|
||||
pr_qty = flt(d.qty) * flt(d.conversion_factor)
|
||||
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
|
||||
if pr_qty:
|
||||
|
||||
@@ -507,7 +533,7 @@ class BuyingController(SubcontractingController):
|
||||
d,
|
||||
{
|
||||
"warehouse": d.rejected_warehouse,
|
||||
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
|
||||
"actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")),
|
||||
"serial_no": cstr(d.rejected_serial_no).strip(),
|
||||
"incoming_rate": 0.0,
|
||||
},
|
||||
@@ -683,6 +709,7 @@ class BuyingController(SubcontractingController):
|
||||
"asset_quantity": row.qty if is_grouped_asset else 0,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
"cost_center": row.cost_center,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -30,10 +30,16 @@ def set_print_templates_for_taxes(doc, settings):
|
||||
doc.print_templates.update(
|
||||
{
|
||||
"total": "templates/print_formats/includes/total.html",
|
||||
"taxes": "templates/print_formats/includes/taxes.html",
|
||||
}
|
||||
)
|
||||
|
||||
if not doc.should_show_taxes_as_table_in_print():
|
||||
doc.print_templates.update(
|
||||
{
|
||||
"taxes": "templates/print_formats/includes/taxes.html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def format_columns(display_columns, compact_fields):
|
||||
compact_fields = compact_fields + ["image", "item_code", "item_name"]
|
||||
|
||||
@@ -617,6 +617,9 @@ def get_filters(
|
||||
if reference_voucher_detail_no:
|
||||
filters["voucher_detail_no"] = reference_voucher_detail_no
|
||||
|
||||
if item_row and item_row.get("warehouse"):
|
||||
filters["warehouse"] = item_row.get("warehouse")
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
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, get_link_to_form, has_gravatar, validate_email_address
|
||||
@@ -43,9 +46,8 @@ class Lead(SellingController, CRMNote):
|
||||
self.update_prospect()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
frappe.db.set_value("Issue", {"lead": self.name}, "lead", None)
|
||||
delete_contact_and_address(self.doctype, self.name)
|
||||
self.remove_link_from_prospect()
|
||||
|
||||
def set_full_name(self):
|
||||
@@ -122,27 +124,6 @@ class Lead(SellingController, CRMNote):
|
||||
)
|
||||
lead_row.db_update()
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": self.doctype, "link_name": self.name},
|
||||
fields=["parent", "parenttype"],
|
||||
)
|
||||
|
||||
for link in links:
|
||||
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
|
||||
|
||||
if len(linked_doc.get("links")) == 1:
|
||||
linked_doc.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in linked_doc.get("links"):
|
||||
if d.link_doctype == self.doctype and d.link_name == self.name:
|
||||
to_remove = d
|
||||
if to_remove:
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def remove_link_from_prospect(self):
|
||||
prospects = self.get_linked_prospects()
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
@@ -16,7 +19,7 @@ class Prospect(CRMNote):
|
||||
self.link_with_lead_contact_and_address()
|
||||
|
||||
def on_trash(self):
|
||||
self.unlink_dynamic_links()
|
||||
delete_contact_and_address(self.doctype, self.name)
|
||||
|
||||
def after_insert(self):
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
@@ -54,27 +57,6 @@ class Prospect(CRMNote):
|
||||
linked_doc.append("links", {"link_doctype": self.doctype, "link_name": self.name})
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": self.doctype, "link_name": self.name},
|
||||
fields=["parent", "parenttype"],
|
||||
)
|
||||
|
||||
for link in links:
|
||||
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
|
||||
|
||||
if len(linked_doc.get("links")) == 1:
|
||||
linked_doc.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in linked_doc.get("links"):
|
||||
if d.link_doctype == self.doctype and d.link_name == self.name:
|
||||
to_remove = d
|
||||
if to_remove:
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name, target_doc=None):
|
||||
|
||||
@@ -78,9 +78,10 @@ erpnext.ProductList = class {
|
||||
let title_html = `<div style="display: flex; margin-left: -15px;">`;
|
||||
title_html += `
|
||||
<div class="col-8" style="margin-right: -15px;">
|
||||
<a class="" href="/${ item.route || '#' }"
|
||||
style="color: var(--gray-800); font-weight: 500;">
|
||||
<a href="/${ item.route || '#' }">
|
||||
<div class="product-title">
|
||||
${ title }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@@ -201,4 +202,4 @@ erpnext.ProductList = class {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, throw
|
||||
from frappe import _, bold, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
from frappe.utils import cint, cstr, flt, get_fullname
|
||||
@@ -201,6 +201,11 @@ def get_shopping_cart_menu(context=None):
|
||||
@frappe.whitelist()
|
||||
def add_new_address(doc):
|
||||
doc = frappe.parse_json(doc)
|
||||
address_title = doc.get("address_title")
|
||||
if frappe.db.exists("Address", {"address_title": address_title}):
|
||||
msg = f"The address with the title {bold(address_title)} already exists. Please change the title accordingly."
|
||||
frappe.throw(_(msg), title=_("Address Already Exists"))
|
||||
|
||||
doc.update({"doctype": "Address"})
|
||||
address = frappe.get_doc(doc)
|
||||
address.save(ignore_permissions=True)
|
||||
|
||||
@@ -162,6 +162,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
|
||||
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
|
||||
|
||||
if product_info:
|
||||
product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item")
|
||||
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
||||
else:
|
||||
product_info = None
|
||||
|
||||
@@ -290,8 +290,8 @@ def get_last_accrual_date(loan, posting_date):
|
||||
# interest for last interest accrual date is already booked, so add 1 day
|
||||
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
||||
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
|
||||
last_interest_accrual_date
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > add_days(
|
||||
getdate(last_interest_accrual_date), 1
|
||||
):
|
||||
last_interest_accrual_date = last_disbursement_date
|
||||
|
||||
|
||||
@@ -88,12 +88,14 @@ class BOMUpdateLog(Document):
|
||||
boms=boms,
|
||||
timeout=40000,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ frappe.ui.form.on('Job Card', {
|
||||
// and if stock mvt for WIP is required
|
||||
if (frm.doc.work_order) {
|
||||
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
|
||||
if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0) {
|
||||
if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0 || !frm.doc.items.length) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
});
|
||||
@@ -411,6 +411,16 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.total_completed_qty && frm.doc.for_quantity > frm.doc.total_completed_qty) {
|
||||
let flt_precision = precision('for_quantity', frm.doc);
|
||||
let process_loss_qty = (
|
||||
flt(frm.doc.for_quantity, flt_precision)
|
||||
- flt(frm.doc.total_completed_qty, flt_precision)
|
||||
);
|
||||
|
||||
frm.set_value('process_loss_qty', process_loss_qty);
|
||||
}
|
||||
|
||||
refresh_field("total_completed_qty");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"time_logs",
|
||||
"section_break_13",
|
||||
"total_completed_qty",
|
||||
"process_loss_qty",
|
||||
"column_break_15",
|
||||
"total_time_in_mins",
|
||||
"section_break_8",
|
||||
@@ -435,11 +436,17 @@
|
||||
"fieldname": "expected_end_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Expected End Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-23 09:56:43.826602",
|
||||
"modified": "2023-06-09 12:04:55.534264",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
@@ -497,4 +504,4 @@
|
||||
"states": [],
|
||||
"title_field": "operation",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +451,9 @@ class JobCard(Document):
|
||||
},
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.set_process_loss()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_transfer_qty()
|
||||
self.validate_job_card()
|
||||
@@ -487,19 +490,35 @@ class JobCard(Document):
|
||||
)
|
||||
)
|
||||
|
||||
if self.for_quantity and self.total_completed_qty != self.for_quantity:
|
||||
precision = self.precision("total_completed_qty")
|
||||
total_completed_qty = flt(
|
||||
flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision)
|
||||
)
|
||||
|
||||
if self.for_quantity and flt(total_completed_qty, precision) != flt(
|
||||
self.for_quantity, precision
|
||||
):
|
||||
total_completed_qty = bold(_("Total Completed Qty"))
|
||||
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||
|
||||
frappe.throw(
|
||||
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
||||
total_completed_qty,
|
||||
bold(self.total_completed_qty),
|
||||
bold(flt(total_completed_qty, precision)),
|
||||
qty_to_manufacture,
|
||||
bold(self.for_quantity),
|
||||
)
|
||||
)
|
||||
|
||||
def set_process_loss(self):
|
||||
precision = self.precision("total_completed_qty")
|
||||
|
||||
self.process_loss_qty = 0.0
|
||||
if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
|
||||
self.process_loss_qty = flt(self.for_quantity, precision) - flt(
|
||||
self.total_completed_qty, precision
|
||||
)
|
||||
|
||||
def update_work_order(self):
|
||||
if not self.work_order:
|
||||
return
|
||||
@@ -511,7 +530,7 @@ class JobCard(Document):
|
||||
):
|
||||
return
|
||||
|
||||
for_quantity, time_in_mins = 0, 0
|
||||
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
|
||||
from_time_list, to_time_list = [], []
|
||||
|
||||
field = "operation_id"
|
||||
@@ -519,6 +538,7 @@ class JobCard(Document):
|
||||
if data and len(data) > 0:
|
||||
for_quantity = flt(data[0].completed_qty)
|
||||
time_in_mins = flt(data[0].time_in_mins)
|
||||
process_loss_qty = flt(data[0].process_loss_qty)
|
||||
|
||||
wo = frappe.get_doc("Work Order", self.work_order)
|
||||
|
||||
@@ -526,8 +546,8 @@ class JobCard(Document):
|
||||
self.update_corrective_in_work_order(wo)
|
||||
|
||||
elif self.operation_id:
|
||||
self.validate_produced_quantity(for_quantity, wo)
|
||||
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
||||
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
|
||||
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
|
||||
|
||||
def update_corrective_in_work_order(self, wo):
|
||||
wo.corrective_operation_cost = 0.0
|
||||
@@ -542,11 +562,11 @@ class JobCard(Document):
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
wo.save()
|
||||
|
||||
def validate_produced_quantity(self, for_quantity, wo):
|
||||
def validate_produced_quantity(self, for_quantity, process_loss_qty, wo):
|
||||
if self.docstatus < 2:
|
||||
return
|
||||
|
||||
if wo.produced_qty > for_quantity:
|
||||
if wo.produced_qty > for_quantity + process_loss_qty:
|
||||
first_part_msg = _(
|
||||
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
|
||||
).format(
|
||||
@@ -561,7 +581,7 @@ class JobCard(Document):
|
||||
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
|
||||
)
|
||||
|
||||
def update_work_order_data(self, for_quantity, time_in_mins, wo):
|
||||
def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo):
|
||||
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
@@ -582,6 +602,7 @@ class JobCard(Document):
|
||||
for data in wo.operations:
|
||||
if data.get("name") == self.operation_id:
|
||||
data.completed_qty = for_quantity
|
||||
data.process_loss_qty = process_loss_qty
|
||||
data.actual_operation_time = time_in_mins
|
||||
data.actual_start_time = time_data[0].start_time if time_data else None
|
||||
data.actual_end_time = time_data[0].end_time if time_data else None
|
||||
@@ -599,7 +620,11 @@ class JobCard(Document):
|
||||
def get_current_operation_data(self):
|
||||
return frappe.get_all(
|
||||
"Job Card",
|
||||
fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
||||
fields=[
|
||||
"sum(total_time_in_mins) as time_in_mins",
|
||||
"sum(total_completed_qty) as completed_qty",
|
||||
"sum(process_loss_qty) as process_loss_qty",
|
||||
],
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"work_order": self.work_order,
|
||||
@@ -777,7 +802,7 @@ class JobCard(Document):
|
||||
|
||||
data = frappe.get_all(
|
||||
"Work Order Operation",
|
||||
fields=["operation", "status", "completed_qty"],
|
||||
fields=["operation", "status", "completed_qty", "sequence_id"],
|
||||
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
|
||||
order_by="sequence_id, idx",
|
||||
)
|
||||
@@ -795,6 +820,16 @@ class JobCard(Document):
|
||||
OperationSequenceError,
|
||||
)
|
||||
|
||||
if row.completed_qty < current_operation_qty:
|
||||
msg = f"""The completed quantity {bold(current_operation_qty)}
|
||||
of an operation {bold(self.operation)} cannot be greater
|
||||
than the completed quantity {bold(row.completed_qty)}
|
||||
of a previous operation
|
||||
{bold(row.operation)}.
|
||||
"""
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.is_work_order_closed():
|
||||
frappe.throw(_("You can't make any changes to Job Card since Work Order is closed."))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from typing import Literal
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils.data import add_to_date, now, today
|
||||
@@ -469,6 +470,119 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(ste.from_bom, 1.0)
|
||||
self.assertEqual(ste.bom_no, work_order.bom_no)
|
||||
|
||||
def test_job_card_proccess_qty_and_completed_qty(self):
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import (
|
||||
create_routing,
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
operations = [
|
||||
{"operation": "Test Operation A1", "workstation": "Test Workstation A", "time_in_mins": 30},
|
||||
{"operation": "Test Operation B1", "workstation": "Test Workstation A", "time_in_mins": 20},
|
||||
]
|
||||
|
||||
make_test_records("UOM")
|
||||
|
||||
warehouse = create_warehouse("Test Warehouse 123 for Job Card")
|
||||
|
||||
setup_operations(operations)
|
||||
|
||||
item_code = "Test Job Card Process Qty Item"
|
||||
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
|
||||
if not frappe.db.exists("Item", item):
|
||||
make_item(
|
||||
item,
|
||||
{
|
||||
"item_name": item,
|
||||
"stock_uom": "Nos",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
|
||||
bom_doc = setup_bom(
|
||||
item_code=item_code,
|
||||
routing=routing_doc.name,
|
||||
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in bom_doc.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target=row.source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=item_code,
|
||||
bom_no=bom_doc.name,
|
||||
skip_transfer=1,
|
||||
wip_warehouse=warehouse,
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in routing_doc.operations:
|
||||
self.assertEqual(row.sequence_id, row.idx)
|
||||
|
||||
first_job_card = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 1},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", first_job_card)
|
||||
jc.time_logs[0].completed_qty = 8
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
self.assertEqual(jc.process_loss_qty, 2)
|
||||
self.assertEqual(jc.for_quantity, 10)
|
||||
|
||||
second_job_card = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 2},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc2 = frappe.get_doc("Job Card", second_job_card)
|
||||
jc2.time_logs[0].completed_qty = 10
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jc2.save)
|
||||
|
||||
jc2.load_from_db()
|
||||
jc2.time_logs[0].completed_qty = 8
|
||||
jc2.save()
|
||||
jc2.submit()
|
||||
|
||||
self.assertEqual(jc2.for_quantity, 10)
|
||||
self.assertEqual(jc2.process_loss_qty, 2)
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 10))
|
||||
s.submit()
|
||||
|
||||
self.assertEqual(s.process_loss_qty, 2)
|
||||
|
||||
wo_doc.reload()
|
||||
for row in wo_doc.operations:
|
||||
self.assertEqual(row.completed_qty, 8)
|
||||
self.assertEqual(row.process_loss_qty, 2)
|
||||
|
||||
self.assertEqual(wo_doc.produced_qty, 8)
|
||||
self.assertEqual(wo_doc.process_loss_qty, 2)
|
||||
self.assertEqual(wo_doc.status, "Completed")
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
@@ -99,7 +99,7 @@ frappe.ui.form.on('Production Plan', {
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
|
||||
if (frm.doc.mr_items && frm.doc.mr_items.length && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
|
||||
frm.add_custom_button(__("Material Request"), ()=> {
|
||||
frm.trigger("make_material_request");
|
||||
}, __('Create'));
|
||||
|
||||
@@ -515,6 +515,9 @@ class ProductionPlan(Document):
|
||||
self.show_list_created_message("Work Order", wo_list)
|
||||
self.show_list_created_message("Purchase Order", po_list)
|
||||
|
||||
if not wo_list:
|
||||
frappe.msgprint(_("No Work Orders were created"))
|
||||
|
||||
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
|
||||
items_data = self.get_production_items()
|
||||
|
||||
@@ -618,6 +621,9 @@ class ProductionPlan(Document):
|
||||
def create_work_order(self, item):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
|
||||
|
||||
if item.get("qty") <= 0:
|
||||
return
|
||||
|
||||
wo = frappe.new_doc("Work Order")
|
||||
wo.update(item)
|
||||
wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date")
|
||||
|
||||
@@ -76,6 +76,13 @@ class TestProductionPlan(FrappeTestCase):
|
||||
"Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1
|
||||
)
|
||||
|
||||
pln.make_work_order()
|
||||
nwork_orders = frappe.get_all(
|
||||
"Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1
|
||||
)
|
||||
|
||||
self.assertTrue(len(work_orders), len(nwork_orders))
|
||||
|
||||
self.assertTrue(len(work_orders), len(pln.po_items))
|
||||
|
||||
for name in material_requests:
|
||||
|
||||
@@ -141,6 +141,7 @@ def setup_bom(**args):
|
||||
routing=args.routing,
|
||||
with_operations=1,
|
||||
currency=args.currency,
|
||||
source_warehouse=args.source_warehouse,
|
||||
)
|
||||
else:
|
||||
bom_doc = frappe.get_doc("BOM", name)
|
||||
|
||||
@@ -891,7 +891,7 @@ class TestWorkOrder(FrappeTestCase):
|
||||
self.assertEqual(se.process_loss_qty, 1)
|
||||
|
||||
wo.load_from_db()
|
||||
self.assertEqual(wo.status, "In Process")
|
||||
self.assertEqual(wo.status, "Completed")
|
||||
|
||||
@timeout(seconds=60)
|
||||
def test_job_card_scrap_item(self):
|
||||
@@ -1657,6 +1657,61 @@ class TestWorkOrder(FrappeTestCase):
|
||||
job_card2.time_logs = []
|
||||
job_card2.save()
|
||||
|
||||
def test_make_serial_no_batch_from_work_order_for_serial_no(self):
|
||||
item_code = "Test Serial No Item For Work Order"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
raw_materials = [
|
||||
"Test RM Item 1 for Serial No Item In Work Order",
|
||||
]
|
||||
|
||||
make_item(
|
||||
item_code,
|
||||
{
|
||||
"has_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TSNIFWO-.#####",
|
||||
},
|
||||
)
|
||||
|
||||
for rm_item in raw_materials:
|
||||
make_item(
|
||||
rm_item,
|
||||
{
|
||||
"has_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
test_stock_entry.make_stock_entry(item_code=rm_item, target=warehouse, qty=10, basic_rate=100)
|
||||
|
||||
bom = make_bom(item=item_code, raw_materials=raw_materials)
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 1)
|
||||
|
||||
wo_order = make_wo_order_test_record(
|
||||
item=item_code,
|
||||
bom_no=bom.name,
|
||||
qty=5,
|
||||
skip_transfer=1,
|
||||
from_wip_warehouse=1,
|
||||
)
|
||||
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"item_code": item_code, "work_order": wo_order.name},
|
||||
)
|
||||
|
||||
serial_nos = [d.name for d in serial_nos]
|
||||
self.assertEqual(len(serial_nos), 5)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
|
||||
|
||||
stock_entry.submit()
|
||||
for row in stock_entry.items:
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)
|
||||
|
||||
|
||||
def prepare_data_for_workstation_type_check():
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
@@ -1886,6 +1941,7 @@ def make_wo_order_test_record(**args):
|
||||
wo_order.sales_order = args.sales_order or None
|
||||
wo_order.planned_start_date = args.planned_start_date or now()
|
||||
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
||||
wo_order.from_wip_warehouse = args.from_wip_warehouse or None
|
||||
|
||||
if args.source_warehouse:
|
||||
for item in wo_order.get("required_items"):
|
||||
|
||||
@@ -139,7 +139,7 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
|
||||
if (frm.doc.status != "Closed") {
|
||||
if (frm.doc.docstatus === 1
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed"
|
||||
&& frm.doc.operations && frm.doc.operations.length) {
|
||||
|
||||
const not_completed = frm.doc.operations.filter(d => {
|
||||
@@ -256,6 +256,12 @@ frappe.ui.form.on("Work Order", {
|
||||
label: __('Batch Size'),
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'sequence_id',
|
||||
label: __('Sequence Id'),
|
||||
read_only: 1
|
||||
},
|
||||
],
|
||||
data: operations_data,
|
||||
in_place_edit: true,
|
||||
@@ -280,8 +286,8 @@ frappe.ui.form.on("Work Order", {
|
||||
|
||||
var pending_qty = 0;
|
||||
frm.doc.operations.forEach(data => {
|
||||
if(data.completed_qty != frm.doc.qty) {
|
||||
pending_qty = frm.doc.qty - flt(data.completed_qty);
|
||||
if(data.completed_qty + data.process_loss_qty != frm.doc.qty) {
|
||||
pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty);
|
||||
|
||||
if (pending_qty) {
|
||||
dialog.fields_dict.operations.df.data.push({
|
||||
@@ -290,7 +296,8 @@ frappe.ui.form.on("Work Order", {
|
||||
'workstation': data.workstation,
|
||||
'batch_size': data.batch_size,
|
||||
'qty': pending_qty,
|
||||
'pending_qty': pending_qty
|
||||
'pending_qty': pending_qty,
|
||||
'sequence_id': data.sequence_id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
"required_items_section",
|
||||
"materials_and_operations_tab",
|
||||
"operations_section",
|
||||
"operations",
|
||||
"transfer_material_against",
|
||||
"operations",
|
||||
"time",
|
||||
"planned_start_date",
|
||||
"planned_end_date",
|
||||
@@ -331,7 +331,6 @@
|
||||
"label": "Expected Delivery Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "operations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operations",
|
||||
@@ -599,7 +598,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 12:35:12.149827",
|
||||
"modified": "2023-06-09 13:20:09.154362",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -249,7 +249,9 @@ class WorkOrder(Document):
|
||||
status = "Not Started"
|
||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||
status = "In Process"
|
||||
if flt(self.produced_qty) >= flt(self.qty):
|
||||
|
||||
total_qty = flt(self.produced_qty) + flt(self.process_loss_qty)
|
||||
if flt(total_qty) >= flt(self.qty):
|
||||
status = "Completed"
|
||||
else:
|
||||
status = "Cancelled"
|
||||
@@ -736,13 +738,15 @@ class WorkOrder(Document):
|
||||
max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty))
|
||||
|
||||
for d in self.get("operations"):
|
||||
if not d.completed_qty:
|
||||
precision = d.precision("completed_qty")
|
||||
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
|
||||
if not qty:
|
||||
d.status = "Pending"
|
||||
elif flt(d.completed_qty) < flt(self.qty):
|
||||
elif flt(qty) < flt(self.qty):
|
||||
d.status = "Work in Progress"
|
||||
elif flt(d.completed_qty) == flt(self.qty):
|
||||
elif flt(qty) == flt(self.qty):
|
||||
d.status = "Completed"
|
||||
elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
|
||||
elif flt(qty) <= max_allowed_qty_for_wo:
|
||||
d.status = "Completed"
|
||||
else:
|
||||
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
"actions": [],
|
||||
"creation": "2014-10-16 14:35:41.950175",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"details",
|
||||
"operation",
|
||||
"status",
|
||||
"completed_qty",
|
||||
"process_loss_qty",
|
||||
"column_break_4",
|
||||
"bom",
|
||||
"workstation_type",
|
||||
@@ -36,6 +38,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -46,6 +49,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -62,7 +66,7 @@
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"columns": 2,
|
||||
"description": "Operation completed for how many finished goods?",
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
@@ -80,6 +84,7 @@
|
||||
"options": "Pending\nWork in Progress\nCompleted"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "workstation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -115,7 +120,7 @@
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation Time",
|
||||
"label": "Time",
|
||||
"oldfieldname": "time_in_mins",
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
@@ -203,12 +208,21 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Workstation Type",
|
||||
"options": "Workstation Type"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Process Loss Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-09 01:37:56.563068",
|
||||
"modified": "2023-06-09 14:03:01.612909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
|
||||
@@ -33,10 +33,9 @@ def get_data(filters: Filters) -> Data:
|
||||
wo.name,
|
||||
wo.status,
|
||||
wo.production_item,
|
||||
wo.qty,
|
||||
wo.produced_qty,
|
||||
wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
|
||||
wo.qty.as_("qty_to_manufacture"),
|
||||
Sum(se.total_incoming_value).as_("total_fg_value"),
|
||||
Sum(se.total_outgoing_value).as_("total_rm_value"),
|
||||
)
|
||||
@@ -44,6 +43,7 @@ def get_data(filters: Filters) -> Data:
|
||||
(wo.process_loss_qty > 0)
|
||||
& (wo.company == filters.company)
|
||||
& (se.docstatus == 1)
|
||||
& (se.purpose == "Manufacture")
|
||||
& (se.posting_date.between(filters.from_date, filters.to_date))
|
||||
)
|
||||
.groupby(se.work_order)
|
||||
@@ -79,20 +79,30 @@ def get_columns() -> Columns:
|
||||
"width": "100",
|
||||
},
|
||||
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": "100"},
|
||||
{
|
||||
"label": _("Qty To Manufacture"),
|
||||
"fieldname": "qty_to_manufacture",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{
|
||||
"label": _("Manufactured Qty"),
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{"label": _("Loss Qty"), "fieldname": "process_loss_qty", "fieldtype": "Float", "width": "150"},
|
||||
{
|
||||
"label": _("Actual Manufactured Qty"),
|
||||
"fieldname": "actual_produced_qty",
|
||||
"label": _("Process Loss Qty"),
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{
|
||||
"label": _("Process Loss Value"),
|
||||
"fieldname": "total_pl_value",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{"label": _("Loss Value"), "fieldname": "total_pl_value", "fieldtype": "Float", "width": "150"},
|
||||
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
|
||||
{
|
||||
"label": _("Raw Material Value"),
|
||||
@@ -105,5 +115,5 @@ def get_columns() -> Columns:
|
||||
|
||||
def update_data_with_total_pl_value(data: Data) -> None:
|
||||
for row in data:
|
||||
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
|
||||
value_per_unit_fg = row["total_fg_value"] / row["qty_to_manufacture"]
|
||||
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
|
||||
|
||||
@@ -333,3 +333,5 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.patches.v14_0.update_company_in_ldc
|
||||
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
|
||||
erpnext.patches.v14_0.cleanup_workspaces
|
||||
erpnext.patches.v14_0.enable_allow_existing_serial_no
|
||||
erpnext.patches.v14_0.set_report_in_process_SOA
|
||||
|
||||
9
erpnext/patches/v14_0/enable_allow_existing_serial_no.py
Normal file
9
erpnext/patches/v14_0/enable_allow_existing_serial_no.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("stock", "doctype", frappe.scrub("Stock Settings"))
|
||||
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1, update_modified=False)
|
||||
10
erpnext/patches/v14_0/set_report_in_process_SOA.py
Normal file
10
erpnext/patches/v14_0/set_report_in_process_SOA.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
process_soa = frappe.qb.DocType("Process Statement Of Accounts")
|
||||
q = frappe.qb.update(process_soa).set(process_soa.report, "General Ledger")
|
||||
q.run()
|
||||
@@ -805,11 +805,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
);
|
||||
}
|
||||
|
||||
this.frm.doc.payments.find(pay => {
|
||||
if (pay.default) {
|
||||
pay.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
if(!this.frm.doc.is_return){
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -123,6 +123,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
|
||||
return me.set_query_for_batch(doc, cdt, cdn);
|
||||
});
|
||||
|
||||
let batch_field = this.frm.get_docfield('items', 'batch_no');
|
||||
if (batch_field) {
|
||||
batch_field.get_route_options_for_new_doc = (row) => {
|
||||
return {
|
||||
'item': row.doc.item_code
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if(
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2022-11-08 15:52:34.462657",
|
||||
"modified": "2023-05-09 15:38:40.255193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
|
||||
@@ -299,6 +299,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
)
|
||||
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
target.delivery_date = nowdate()
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
@@ -306,6 +307,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
|
||||
target.qty = balance_qty if balance_qty > 0 else 0
|
||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||
target.delivery_date = nowdate()
|
||||
|
||||
if obj.against_blanket_order:
|
||||
target.against_blanket_order = obj.against_blanket_order
|
||||
|
||||
@@ -60,9 +60,9 @@ class TestQuotation(FrappeTestCase):
|
||||
sales_order = make_sales_order(quotation.name)
|
||||
sales_order.currency = "USD"
|
||||
sales_order.conversion_rate = 20.0
|
||||
sales_order.delivery_date = "2019-01-01"
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
self.assertEqual(sales_order.currency, "USD")
|
||||
@@ -644,8 +644,6 @@ def make_quotation(**args):
|
||||
},
|
||||
)
|
||||
|
||||
qo.delivery_date = add_days(qo.transaction_date, 10)
|
||||
|
||||
if not args.do_not_save:
|
||||
qo.insert()
|
||||
if not args.do_not_submit:
|
||||
|
||||
@@ -158,7 +158,8 @@ class SalesOrder(SellingController):
|
||||
frappe.msgprint(
|
||||
_("Expected Delivery Date should be after Sales Order Date"),
|
||||
indicator="orange",
|
||||
title=_("Warning"),
|
||||
title=_("Invalid Delivery Date"),
|
||||
raise_exception=True,
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Please enter Delivery Date"))
|
||||
@@ -217,6 +218,7 @@ class SalesOrder(SellingController):
|
||||
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
|
||||
|
||||
doc.set_status(update=True)
|
||||
doc.update_opportunity("Converted" if flag == "submit" else "Quotation")
|
||||
|
||||
def validate_drop_ship(self):
|
||||
for d in self.get("items"):
|
||||
|
||||
@@ -49,7 +49,6 @@ def search_by_term(search_term, warehouse, price_list):
|
||||
)
|
||||
|
||||
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
|
||||
item_stock_qty = item_stock_qty // item.get("conversion_factor")
|
||||
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
|
||||
item.update({"actual_qty": item_stock_qty})
|
||||
|
||||
@@ -59,7 +58,7 @@ def search_by_term(search_term, warehouse, price_list):
|
||||
"price_list": price_list,
|
||||
"item_code": item_code,
|
||||
},
|
||||
fields=["uom", "stock_uom", "currency", "price_list_rate"],
|
||||
fields=["uom", "currency", "price_list_rate"],
|
||||
)
|
||||
|
||||
def __sort(p):
|
||||
|
||||
@@ -78,7 +78,9 @@
|
||||
"salary_mode",
|
||||
"bank_details_section",
|
||||
"bank_name",
|
||||
"column_break_heye",
|
||||
"bank_ac_no",
|
||||
"iban",
|
||||
"personal_details",
|
||||
"marital_status",
|
||||
"family_background",
|
||||
@@ -804,17 +806,26 @@
|
||||
{
|
||||
"fieldname": "column_break_104",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_heye",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.salary_mode == 'Bank'",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2022-09-13 10:27:14.579197",
|
||||
"modified": "2023-03-30 15:57:05.174592",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
||||
@@ -215,13 +215,39 @@ def hide_workspaces():
|
||||
|
||||
|
||||
def create_default_role_profiles():
|
||||
for module in ["Accounts", "Stock", "Manufacturing"]:
|
||||
create_role_profile(module)
|
||||
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
role_profile.role_profile = role_profile_name
|
||||
for role in roles:
|
||||
role_profile.append("roles", {"role": role})
|
||||
|
||||
role_profile.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_role_profile(module):
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
role_profile.role_profile = _("{0} User").format(module)
|
||||
role_profile.append("roles", {"role": module + " User"})
|
||||
role_profile.append("roles", {"role": module + " Manager"})
|
||||
role_profile.insert()
|
||||
DEFAULT_ROLE_PROFILES = {
|
||||
"Inventory": [
|
||||
"Stock User",
|
||||
"Stock Manager",
|
||||
"Item Manager",
|
||||
],
|
||||
"Manufacturing": [
|
||||
"Stock User",
|
||||
"Manufacturing User",
|
||||
"Manufacturing Manager",
|
||||
],
|
||||
"Accounts": [
|
||||
"Accounts User",
|
||||
"Accounts Manager",
|
||||
],
|
||||
"Sales": [
|
||||
"Sales User",
|
||||
"Stock User",
|
||||
"Sales Manager",
|
||||
],
|
||||
"Purchase": [
|
||||
"Item Manager",
|
||||
"Stock User",
|
||||
"Purchase User",
|
||||
"Purchase Manager",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class ClosingStockBalance(Document):
|
||||
|
||||
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
|
||||
if self.get(fieldname):
|
||||
query = query.where(table.get(fieldname) == self.get(fieldname))
|
||||
query = query.where(table[fieldname] == self.get(fieldname))
|
||||
|
||||
query = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -1222,7 +1222,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Pick List",
|
||||
"options": "Pick List",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1398,7 +1399,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 11:15:23.931084",
|
||||
"modified": "2023-06-16 14:58:55.066602",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -66,8 +66,7 @@
|
||||
"fieldname": "driver",
|
||||
"fieldtype": "Link",
|
||||
"label": "Driver",
|
||||
"options": "Driver",
|
||||
"reqd": 1
|
||||
"options": "Driver"
|
||||
},
|
||||
{
|
||||
"fetch_from": "driver.full_name",
|
||||
@@ -189,10 +188,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-30 21:21:36.610142",
|
||||
"modified": "2023-06-27 11:22:27.927637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Trip",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -228,5 +228,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "driver_name"
|
||||
}
|
||||
@@ -24,6 +24,9 @@ class DeliveryTrip(Document):
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if self._action == "submit" and not self.driver:
|
||||
frappe.throw(_("A driver must be set to submit."))
|
||||
|
||||
self.validate_stop_addresses()
|
||||
|
||||
def on_submit(self):
|
||||
|
||||
@@ -714,6 +714,7 @@ class Item(Document):
|
||||
template=self,
|
||||
now=frappe.flags.in_test,
|
||||
timeout=600,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
def validate_has_variants(self):
|
||||
|
||||
@@ -36,7 +36,7 @@ class PickList(Document):
|
||||
for location in self.get("locations"):
|
||||
if (
|
||||
location.sales_order
|
||||
and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100
|
||||
and frappe.db.get_value("Sales Order", location.sales_order, "per_picked", cache=True) == 100
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row #{}: item {} has been picked already.").format(location.idx, location.item_code)
|
||||
@@ -360,6 +360,7 @@ class PickList(Document):
|
||||
(pi_item.item_code.isin([x.item_code for x in items]))
|
||||
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||
& (pi.status != "Completed")
|
||||
& (pi.status != "Cancelled")
|
||||
& (pi_item.docstatus != 2)
|
||||
)
|
||||
.groupby(
|
||||
@@ -473,7 +474,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
||||
)
|
||||
qty = stock_qty / (item_doc.conversion_factor or 1)
|
||||
|
||||
uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number")
|
||||
uom_must_be_whole_number = frappe.get_cached_value("UOM", item_doc.uom, "must_be_whole_number")
|
||||
if uom_must_be_whole_number:
|
||||
qty = floor(qty)
|
||||
stock_qty = qty * item_doc.conversion_factor
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user