Merge branch 'develop' into Provision-to-send-Accounts-Receivable-Reports

This commit is contained in:
Deepesh Garg 2023-06-23 15:58:38 +05:30 committed by GitHub
commit df035f6b19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1168 additions and 358 deletions

View File

@ -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

View File

@ -61,7 +61,10 @@
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
"show_balance_in_coa"
"show_balance_in_coa",
"banking_tab",
"enable_party_matching",
"enable_fuzzy_matching"
],
"fields": [
{
@ -383,6 +386,26 @@
"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",
@ -390,7 +413,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-06-13 18:47:46.430291",
"modified": "2023-06-15 16:35:45.123456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -70,7 +70,6 @@ def make_bank_account(doctype, docname):
return doc
@frappe.whitelist()
def get_party_bank_account(party_type, party):
return frappe.db.get_value(party_type, party, "default_bank_account")

View File

@ -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,

View 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

View File

@ -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
}
}

View File

@ -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():

View File

@ -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

View File

@ -37,7 +37,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
validate_rounding_loss: function(frm) {
let allowance = frm.doc.rounding_loss_allowance;
if (!(allowance > 0 && allowance < 1)) {
if (!(allowance >= 0 && allowance < 1)) {
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
}
},

View File

@ -100,15 +100,16 @@
},
{
"default": "0.05",
"description": "Only values between 0 and 1 are allowed. \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",
"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"
"label": "Rounding Loss Allowance",
"precision": "9"
}
],
"is_submittable": 1,
"links": [],
"modified": "2023-06-12 21:02:09.818208",
"modified": "2023-06-20 07:29:06.972434",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",

View File

@ -22,7 +22,7 @@ class ExchangeRateRevaluation(Document):
self.set_total_gain_loss()
def validate_rounding_loss_allowance(self):
if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
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):
@ -373,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"
@ -388,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()

View File

@ -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",

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule'];
},
refresh: function(frm) {

View File

@ -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
@ -370,6 +368,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 (

View File

@ -155,6 +155,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
},
validate_company: (frm) => {

View File

@ -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");
}
});
}
}
});
}

View File

@ -252,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)

View File

@ -54,9 +54,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
hide_fields(this.frm.doc);
// Show / Hide button
this.show_general_ledger();
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
if(doc.update_stock==1 && doc.docstatus==1) {
if(doc.update_stock==1) {
this.show_stock_ledger();
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){

View File

@ -88,8 +88,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
this.show_general_ledger();
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
if(doc.update_stock) this.show_stock_ledger();
if(doc.update_stock){
this.show_stock_ledger();
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {

View File

@ -320,6 +320,7 @@
},
{
"default": "0",
"depends_on": "eval: !doc.is_debit_note",
"fieldname": "is_return",
"fieldtype": "Check",
"hide_days": 1,
@ -1960,6 +1961,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",
@ -2155,7 +2157,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-06-03 16:22:16.219333",
"modified": "2023-06-21 16:02:18.988799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -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
}

View File

@ -87,7 +87,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,
@ -241,7 +241,7 @@ def get_columns(additional_table_columns, filters):
},
{
"label": _("Purchase Receipt"),
"fieldname": "Purchase Receipt",
"fieldname": "purchase_receipt",
"fieldtype": "Link",
"options": "Purchase Receipt",
"width": 100,

View File

@ -237,11 +237,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(

View File

@ -159,15 +159,15 @@ def make_depreciation_entry(asset_depr_schedule_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(asset_depr_schedule_doc.finance_book_id)
row = asset.get("finance_books")[idx - 1]
row.value_after_depreciation -= d.depreciation_amount
row.db_update()
if not je.meta.get_workflow():
je.submit()
idx = cint(asset_depr_schedule_doc.finance_book_id)
row = asset.get("finance_books")[idx - 1]
row.value_after_depreciation -= d.depreciation_amount
row.db_update()
asset.db_set("depr_entry_posting_status", "Successful")

View File

@ -15,7 +15,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();
@ -129,10 +128,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") {
@ -247,26 +242,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;

View File

@ -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-10-12 15:09:40.771332",
"modified": "2023-06-22 14:17:07.995120",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",

View File

@ -19,9 +19,6 @@ from erpnext.assets.doctype.asset.depreciation import (
reverse_depreciation_entry_made_after_disposal,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
from erpnext.controllers.stock_controller import StockController
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@ -45,7 +42,6 @@ force_fields = [
"target_has_batch_no",
"target_stock_uom",
"stock_uom",
"target_fixed_asset_account",
"fixed_asset_account",
"valuation_rate",
]
@ -56,7 +52,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()
@ -71,11 +66,12 @@ 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 = (
@ -86,7 +82,7 @@ class AssetCapitalization(StockController):
)
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
@ -97,15 +93,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())
@ -157,9 +144,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:
@ -170,17 +154,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:
@ -386,7 +359,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):
@ -429,11 +406,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
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format(
@ -519,40 +499,46 @@ 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
notes = _(
"This schedule was created when target Asset {0} was updated through Asset Capitalization {1}."
).format(
get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name)
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes)
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)
notes = _(
"This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
)
reset_depreciation_schedule(asset, self.posting_date, notes)
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)
notes = _(
"This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
)
reset_depreciation_schedule(asset, self.posting_date, notes)
def set_consumed_asset_status(self, asset):
if self.docstatus == 1:
@ -602,33 +588,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, str):

View File

@ -47,13 +47,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,
@ -65,7 +58,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,
@ -94,7 +88,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)
@ -142,13 +136,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,
@ -160,7 +147,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,
@ -189,7 +177,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)
@ -364,6 +352,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,

View File

@ -42,7 +42,6 @@ class AssetMaintenance(Document):
maintenance_log.db_set("maintenance_status", "Cancelled")
@frappe.whitelist()
def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, next_due_date):
team_member = frappe.db.get_value("User", assign_to_member, "email")
args = {

View File

@ -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'",
},
]
};

View File

@ -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,59 +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
ads.asset, SUM(depreciation_amount)
FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
WHERE
ds.parent = ads.name
AND ifnull(ads.finance_book, '')=%s
AND ads.docstatus=1
AND ds.parentfield='depreciation_schedule'
AND ds.schedule_date<=%s
AND ds.journal_entry IS NOT NULL
GROUP BY ads.asset""",
(cstr(filters.finance_book or ""), date),
)
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():

View File

@ -457,7 +457,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-02-18 11:05:50.592270",
"modified": "2023-05-09 15:34:13.408932",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@ -99,7 +99,6 @@ def import_string_path(path):
return mod
@frappe.whitelist()
def make_supplier_scorecard(source_name, target_doc=None):
def update_criteria_fields(obj, target, source_parent):
target.max_score, target.formula = frappe.db.get_value(

View File

@ -320,7 +320,9 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
return data[0]
def make_return_doc(doctype: str, source_name: str, target_doc=None):
def make_return_doc(
doctype: str, source_name: str, target_doc=None, return_against_rejected_qty=False
):
from frappe.model.mapper import get_mapped_doc
company = frappe.db.get_value("Delivery Note", source_name, "company")
@ -471,7 +473,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0))
if hasattr(target_doc, "stock_qty"):
if hasattr(target_doc, "stock_qty") and not return_against_rejected_qty:
target_doc.stock_qty = -1 * flt(
source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)
)
@ -490,6 +492,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.purchase_receipt_item = source_doc.name
if doctype == "Purchase Receipt" and return_against_rejected_qty:
target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0))
target_doc.rejected_qty = 0.0
target_doc.rejected_warehouse = ""
target_doc.warehouse = source_doc.rejected_warehouse
target_doc.received_qty = target_doc.qty
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype

View File

@ -845,6 +845,149 @@ class StockController(AccountsController):
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
@frappe.whitelist()
def show_accounting_ledger_preview(company, doctype, docname):
filters = {"company": company, "include_dimensions": 1}
doc = frappe.get_doc(doctype, docname)
gl_columns, gl_data = get_accounting_ledger_preview(doc, filters)
frappe.db.rollback()
return {"gl_columns": gl_columns, "gl_data": gl_data}
@frappe.whitelist()
def show_stock_ledger_preview(company, doctype, docname):
filters = {"company": company}
doc = frappe.get_doc(doctype, docname)
sl_columns, sl_data = get_stock_ledger_preview(doc, filters)
frappe.db.rollback()
return {
"sl_columns": sl_columns,
"sl_data": sl_data,
}
def get_accounting_ledger_preview(doc, filters):
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
gl_columns, gl_data = [], []
fields = [
"posting_date",
"account",
"debit",
"credit",
"against",
"party",
"party_type",
"cost_center",
"against_voucher_type",
"against_voucher",
]
doc.docstatus = 1
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
doc.update_stock_ledger()
doc.make_gl_entries()
columns = get_gl_columns(filters)
gl_entries = get_gl_entries_for_preview(doc.doctype, doc.name, fields)
gl_columns = get_columns(columns, fields)
gl_data = get_data(fields, gl_entries)
return gl_columns, gl_data
def get_stock_ledger_preview(doc, filters):
from erpnext.stock.report.stock_ledger.stock_ledger import get_columns as get_sl_columns
sl_columns, sl_data = [], []
fields = [
"item_code",
"stock_uom",
"actual_qty",
"qty_after_transaction",
"warehouse",
"incoming_rate",
"valuation_rate",
"stock_value",
"stock_value_difference",
]
columns_fields = [
"item_code",
"stock_uom",
"in_qty",
"out_qty",
"qty_after_transaction",
"warehouse",
"incoming_rate",
"in_out_rate",
"stock_value",
"stock_value_difference",
]
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
doc.docstatus = 1
doc.update_stock_ledger()
columns = get_sl_columns(filters)
sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields)
sl_columns = get_columns(columns, columns_fields)
sl_data = get_data(columns_fields, sl_entries)
return sl_columns, sl_data
def get_sl_entries_for_preview(doctype, docname, fields):
sl_entries = frappe.get_all(
"Stock Ledger Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields
)
for entry in sl_entries:
if entry.actual_qty > 0:
entry["in_qty"] = entry.actual_qty
entry["out_qty"] = 0
else:
entry["out_qty"] = abs(entry.actual_qty)
entry["in_qty"] = 0
entry["in_out_rate"] = entry["valuation_rate"]
return sl_entries
def get_gl_entries_for_preview(doctype, docname, fields):
return frappe.get_all(
"GL Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields
)
def get_columns(raw_columns, fields):
return [
{"name": d.get("label"), "editable": False, "width": 110}
for d in raw_columns
if not d.get("hidden") and d.get("fieldname") in fields
]
def get_data(raw_columns, raw_data):
datatable_data = []
for row in raw_data:
data_row = []
for column in raw_columns:
data_row.append(row.get(column) or "")
datatable_data.append(data_row)
return datatable_data
def repost_required_for_queue(doc: StockController) -> bool:
"""check if stock document contains repeated item-warehouse with queue based valuation.

View File

@ -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

View File

@ -161,7 +161,6 @@ def add_account_subtype(account_subtype):
frappe.throw(frappe.get_traceback())
@frappe.whitelist()
def sync_transactions(bank, bank_account):
"""Sync transactions based on the last integration date as the start date, after sync is completed
add the transaction date of the oldest transaction as the last integration date."""

View File

@ -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'));

View File

@ -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")

View File

@ -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:

View File

@ -66,7 +66,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
}
show_general_ledger() {
var me = this;
let me = this;
if(this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(__('Accounting Ledger'), function() {
frappe.route_options = {

View File

@ -17,6 +17,7 @@ import "./utils/customer_quick_entry";
import "./utils/supplier_quick_entry";
import "./call_popup/call_popup";
import "./utils/dimension_tree_filter";
import "./utils/ledger_preview.js"
import "./utils/barcode_scanner";
import "./telephony";
import "./templates/call_link.html";

View File

@ -0,0 +1,78 @@
frappe.provide('erpnext.accounts');
erpnext.accounts.ledger_preview = {
show_accounting_ledger_preview(frm) {
let me = this;
if(!frm.is_new() && frm.doc.docstatus == 0) {
frm.add_custom_button(__('Accounting Ledger'), function() {
frappe.call({
"type": "GET",
"method": "erpnext.controllers.stock_controller.show_accounting_ledger_preview",
"args": {
"company": frm.doc.company,
"doctype": frm.doc.doctype,
"docname": frm.doc.name
},
"callback": function(response) {
me.make_dialog("Accounting Ledger Preview", "accounting_ledger_preview_html", response.message.gl_columns, response.message.gl_data);
}
})
}, __("Preview"));
}
},
show_stock_ledger_preview(frm) {
let me = this
if(!frm.is_new() && frm.doc.docstatus == 0) {
frm.add_custom_button(__('Stock Ledger'), function() {
frappe.call({
"type": "GET",
"method": "erpnext.controllers.stock_controller.show_stock_ledger_preview",
"args": {
"company": frm.doc.company,
"doctype": frm.doc.doctype,
"docname": frm.doc.name
},
"callback": function(response) {
me.make_dialog("Stock Ledger Preview", "stock_ledger_preview_html", response.message.sl_columns, response.message.sl_data);
}
})
}, __("Preview"));
}
},
make_dialog(label, fieldname, columns, data) {
let me = this;
let dialog = new frappe.ui.Dialog({
"size": "extra-large",
"title": __(label),
"fields": [
{
"fieldtype": "HTML",
"fieldname": fieldname,
},
]
});
setTimeout(function() {
me.get_datatable(columns, data, dialog.get_field(fieldname).wrapper);
}, 200);
dialog.show();
},
get_datatable(columns, data, wrapper) {
const datatable_options = {
columns: columns,
data: data,
dynamicRowHeight: true,
checkboxColumn: false,
inlineFilters: true,
};
new frappe.DataTable(
wrapper,
datatable_options
);
}
}

View File

@ -568,7 +568,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-02-18 11:04:46.343527",
"modified": "2023-05-09 15:38:40.255193",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@ -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": [

View File

@ -200,6 +200,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn
}
}
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
if (doc.docstatus > 0) {
this.show_stock_ledger();
if (erpnext.is_perpetual_inventory_enabled(doc.company)) {

View File

@ -121,6 +121,10 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
refresh() {
var me = this;
super.refresh();
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
if(this.frm.doc.docstatus > 0) {
this.show_stock_ledger();
//removed for temporary
@ -209,10 +213,43 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
}
make_purchase_return() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
frm: cur_frm
let me = this;
let has_rejected_items = cur_frm.doc.items.filter((item) => {
if (item.rejected_qty > 0) {
return true;
}
})
if (has_rejected_items && has_rejected_items.length > 0) {
frappe.prompt([
{
label: __("Return Qty from Rejected Warehouse"),
fieldtype: "Check",
fieldname: "return_for_rejected_warehouse",
default: 1
},
], function(values){
if (values.return_for_rejected_warehouse) {
frappe.call({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return_against_rejected_warehouse",
args: {
source_name: cur_frm.doc.name
},
callback: function(r) {
if(r.message) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
})
} else {
cur_frm.cscript._make_purchase_return();
}
}, __("Return Qty"), __("Make Return Entry"));
} else {
cur_frm.cscript._make_purchase_return();
}
}
close_purchase_receipt() {
@ -322,6 +359,13 @@ frappe.ui.form.on('Purchase Receipt Item', {
},
});
cur_frm.cscript._make_purchase_return = function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
frm: cur_frm
});
}
cur_frm.cscript['Make Stock Entry'] = function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_stock_entry",

View File

@ -1136,6 +1136,13 @@ def get_returned_qty_map(purchase_receipt):
return returned_qty_map
@frappe.whitelist()
def make_purchase_return_against_rejected_warehouse(source_name):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Purchase Receipt", source_name, return_against_rejected_qty=True)
@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc

View File

@ -1827,6 +1827,33 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(abs(data["stock_value_difference"]), 400.00)
def test_return_from_rejected_warehouse(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_return_against_rejected_warehouse,
)
item_code = "_Test Item Return from Rejected Warehouse"
create_item(item_code)
warehouse = create_warehouse("_Test Warehouse Return Qty Warehouse")
rejected_warehouse = create_warehouse("_Test Rejected Warehouse Return Qty Warehouse")
# Step 1: Create Purchase Receipt with valuation rate 100
pr = make_purchase_receipt(
item_code=item_code,
warehouse=warehouse,
qty=10,
rate=100,
rejected_qty=2,
rejected_warehouse=rejected_warehouse,
)
pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
self.assertEqual(pr_return.items[0].warehouse, rejected_warehouse)
self.assertEqual(pr_return.items[0].qty, 2.0 * -1)
self.assertEqual(pr_return.items[0].rejected_qty, 0.0)
self.assertEqual(pr_return.items[0].rejected_warehouse, "")
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@ -13,6 +13,7 @@ from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
import erpnext
from erpnext.accounts.general_ledger import validate_accounting_period
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
from erpnext.stock.stock_ledger import (
get_affected_transactions,
@ -44,11 +45,49 @@ class RepostItemValuation(Document):
self.validate_accounts_freeze()
def validate_period_closing_voucher(self):
# Period Closing Voucher
year_end_date = self.get_max_year_end_date(self.company)
if year_end_date and getdate(self.posting_date) <= getdate(year_end_date):
msg = f"Due to period closing, you cannot repost item valuation before {year_end_date}"
date = frappe.format(year_end_date, "Date")
msg = f"Due to period closing, you cannot repost item valuation before {date}"
frappe.throw(_(msg))
# Accounting Period
if self.voucher_type:
validate_accounting_period(
[
frappe._dict(
{
"posting_date": self.posting_date,
"company": self.company,
"voucher_type": self.voucher_type,
}
)
]
)
# Closing Stock Balance
closing_stock = self.get_closing_stock_balance()
if closing_stock and closing_stock[0].name:
name = get_link_to_form("Closing Stock Balance", closing_stock[0].name)
to_date = frappe.format(closing_stock[0].to_date, "Date")
msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}"
frappe.throw(_(msg))
def get_closing_stock_balance(self):
filters = {
"company": self.company,
"status": "Completed",
"docstatus": 1,
"to_date": (">=", self.posting_date),
}
for field in ["warehouse", "item_code"]:
if self.get(field):
filters.update({field: ("in", ["", self.get(field)])})
return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters)
@staticmethod
def get_max_year_end_date(company):
data = frappe.get_all(

View File

@ -392,3 +392,33 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
pr.cancel()
self.assertTrue(pr.docstatus == 2)
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name}))
def test_repost_item_valuation_for_closing_stock_balance(self):
from erpnext.stock.doctype.closing_stock_balance.closing_stock_balance import (
prepare_closing_stock_balance,
)
doc = frappe.new_doc("Closing Stock Balance")
doc.company = "_Test Company"
doc.from_date = today()
doc.to_date = today()
doc.submit()
prepare_closing_stock_balance(doc.name)
doc.load_from_db()
self.assertEqual(doc.docstatus, 1)
self.assertEqual(doc.status, "Completed")
riv = frappe.new_doc("Repost Item Valuation")
riv.update(
{
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC",
"based_on": "Item and Warehouse",
"posting_date": today(),
"posting_time": "00:01:00",
}
)
self.assertRaises(frappe.ValidationError, riv.save)
doc.cancel()

View File

@ -191,7 +191,6 @@ def process_string_args(args):
return args
@frappe.whitelist()
def get_item_code(barcode=None, serial_no=None):
if barcode:
item_code = frappe.db.get_value("Item Barcode", {"barcode": barcode}, fieldname=["parent"])

View File

@ -475,7 +475,7 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors)
for row_idx, row in enumerate(result):
for convertible_col, data in convertible_column_map.items():
conversion_factor = conversion_factors[row.get("item_code")] or 1
conversion_factor = conversion_factors.get(row.get("item_code")) or 1.0
for_type = data.for_type
value_before_conversion = row.get(convertible_col)
if for_type == "rate":

View File

@ -219,7 +219,8 @@ class ItemConfigure {
: ''
}
${available_qty === 0 ? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
${available_qty === 0 && product_info && product_info?.is_stock_item
? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
</div></div>
<a href data-action="btn_clear_values" data-item-code="${one_item}">
@ -236,7 +237,8 @@ class ItemConfigure {
</div>`;
/* eslint-disable indent */
if (!product_info?.allow_items_not_in_stock && available_qty === 0) {
if (!product_info?.allow_items_not_in_stock && available_qty === 0
&& product_info && product_info?.is_stock_item) {
item_add_to_cart = '';
}

View File

@ -12,6 +12,7 @@ dependencies = [
"pycountry~=22.3.5",
"Unidecode~=1.3.6",
"barcodenumber~=0.5.0",
"rapidfuzz~=2.15.0",
# integration dependencies
"gocardless-pro~=1.22.0",