Merge branch 'develop' of https://github.com/frappe/erpnext into #34282-Record-advance-payment-as-a-liability

This commit is contained in:
Deepesh Garg 2023-06-29 12:19:42 +05:30
commit 0408b6d655
108 changed files with 2014 additions and 834 deletions

View File

@ -43,14 +43,16 @@ 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
with:
node-version: 14
node-version: 18
check-latest: true
- name: Add to Hosts
@ -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

@ -21,7 +21,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 14
node-version: 18
check-latest: true
- name: Check commit titles

View File

@ -71,7 +71,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 14
node-version: 18
check-latest: true
- name: Add to Hosts

View File

@ -59,7 +59,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 14
node-version: 18
check-latest: true
- name: Add to Hosts

View File

@ -68,6 +68,16 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.refresh_field("dimensions");
frm.trigger('setup_filters');
},
apply_restriction_on_values: function(frm) {
/** If restriction on values is not applied, we should set "allow_or_restrict" to "Restrict" with an empty allowed dimension table.
* Hence it's not "restricted" on any value.
*/
if (!frm.doc.apply_restriction_on_values) {
frm.set_value("allow_or_restrict", "Restrict");
frm.clear_table("dimensions");
frm.refresh_field("dimensions");
}
}
});
frappe.ui.form.on('Allowed Dimension', {

View File

@ -10,6 +10,7 @@
"disabled",
"column_break_2",
"company",
"apply_restriction_on_values",
"allow_or_restrict",
"section_break_4",
"accounts",
@ -24,94 +25,80 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Accounting Dimension",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hide_border": 1,
"show_days": 1,
"show_seconds": 1
"hide_border": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.apply_restriction_on_values == 1;",
"fieldname": "allow_or_restrict",
"fieldtype": "Select",
"label": "Allow Or Restrict Dimension",
"options": "Allow\nRestrict",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Applicable On Account",
"options": "Applicable On Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"depends_on": "eval:doc.accounting_dimension",
"depends_on": "eval:doc.accounting_dimension && doc.apply_restriction_on_values",
"fieldname": "dimensions",
"fieldtype": "Table",
"label": "Applicable Dimension",
"options": "Allowed Dimension",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"mandatory_depends_on": "eval:doc.apply_restriction_on_values == 1;",
"options": "Allowed Dimension"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled",
"show_days": 1,
"show_seconds": 1
"label": "Disabled"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "dimension_filter_help",
"fieldtype": "HTML",
"label": "Dimension Filter Help",
"show_days": 1,
"show_seconds": 1
"label": "Dimension Filter Help"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Section Break"
},
{
"default": "1",
"fieldname": "apply_restriction_on_values",
"fieldtype": "Check",
"label": "Apply restriction on dimension values"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-03 12:04:58.678402",
"modified": "2023-06-07 14:59:41.869117",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
@ -154,5 +141,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -8,6 +8,12 @@ from frappe.model.document import Document
class AccountingDimensionFilter(Document):
def before_save(self):
# If restriction is not applied on values, then remove all the dimensions and set allow_or_restrict to Restrict
if not self.apply_restriction_on_values:
self.allow_or_restrict = "Restrict"
self.set("dimensions", [])
def validate(self):
self.validate_applicable_accounts()
@ -44,12 +50,12 @@ def get_dimension_filter_map():
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
FROM
`tabApplicable On Account` a, `tabAllowed Dimension` d,
`tabApplicable On Account` a,
`tabAccounting Dimension Filter` p
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
WHERE
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
""",
as_dict=1,
)
@ -76,4 +82,5 @@ def build_map(map_object, dimension, account, filter_value, allow_or_restrict, i
(dimension, account),
{"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
)
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
if filter_value:
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)

View File

@ -64,6 +64,7 @@ def create_accounting_dimension_filter():
"accounting_dimension": "Cost Center",
"allow_or_restrict": "Allow",
"company": "_Test Company",
"apply_restriction_on_values": 1,
"accounts": [
{
"applicable_on_account": "Sales - _TC",
@ -85,6 +86,7 @@ def create_accounting_dimension_filter():
"doctype": "Accounting Dimension Filter",
"accounting_dimension": "Department",
"allow_or_restrict": "Allow",
"apply_restriction_on_values": 1,
"company": "_Test Company",
"accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
"dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],

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

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

@ -1855,7 +1855,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
if not total_amount:
if party_account_currency == company_currency:
# for handling cases that don't have multi-currency (base field)
total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total")
total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
exchange_rate = 1
else:
total_amount = ref_doc.get("grand_total")

View File

@ -11,6 +11,7 @@ from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry,
get_payment_entry,
get_reference_details,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
make_purchase_invoice,
@ -1037,6 +1038,29 @@ class TestPaymentEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def test_details_update_on_reference_table(self):
so = make_sales_order(
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
)
so.conversion_rate = 50
so.submit()
pe = get_payment_entry("Sales Order", so.name)
pe.references.clear()
pe.paid_from = "Debtors - _TC"
pe.paid_from_account_currency = "INR"
pe.source_exchange_rate = 50
pe.save()
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
expected_response = {
"total_amount": 5000.0,
"outstanding_amount": 5000.0,
"exchange_rate": 1.0,
"due_date": None,
"bill_no": None,
}
self.assertDictEqual(ref_details, expected_response)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@ -96,25 +96,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

@ -268,6 +268,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)
@ -359,7 +363,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:
@ -445,6 +452,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(
{
@ -610,6 +619,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"
@ -654,5 +673,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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,6 +65,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){

View File

@ -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",
@ -67,14 +74,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",
@ -87,6 +94,7 @@
"options": "PSOA Cost Center"
},
{
"depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "project",
"fieldtype": "Table MultiSelect",
"label": "Project",
@ -104,7 +112,7 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"label": "General Ledger Filters"
"label": "Report Filters"
},
{
"fieldname": "column_break_14",
@ -164,12 +172,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",
@ -297,6 +307,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"
@ -310,10 +321,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",

View File

@ -15,6 +15,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,
)
@ -43,29 +44,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 = (
@ -73,60 +55,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,
"show_net_values_in_party_account": doc.show_net_values_in_party_account,
}
)
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
@ -140,6 +87,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",

View File

@ -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 "&nbsp;") %}
{% 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>

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

@ -2157,7 +2157,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-06-19 16:02:05.309332",
"modified": "2023-06-21 16:02:18.988799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -585,7 +585,9 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
"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)",
)
@ -615,7 +617,7 @@ def is_valid_certificate(
):
valid = False
available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_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

View File

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

View File

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

View File

@ -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"):
@ -404,15 +411,18 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
`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
),

View File

@ -0,0 +1,28 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Voucher-wise Balance"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company"
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
"width": "60px"
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
"width": "60px"
},
]
};

View File

@ -0,0 +1,33 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2023-06-27 16:40:15.109554",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"letter_head": "LetterHead",
"modified": "2023-06-27 16:40:32.493725",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Voucher-wise Balance",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Voucher-wise Balance",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@ -0,0 +1,66 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 300},
{
"label": _("Voucher No"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 300,
},
{
"fieldname": "debit",
"label": _("Debit"),
"fieldtype": "Currency",
"options": "currency",
"width": 300,
},
{
"fieldname": "credit",
"label": _("Credit"),
"fieldtype": "Currency",
"options": "currency",
"width": 300,
},
]
def get_data(filters):
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select(
gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
)
.groupby(gle.voucher_no)
)
query = apply_filters(query, filters, gle)
gl_entries = query.run(as_dict=True)
unmatched = [entry for entry in gl_entries if entry.debit != entry.credit]
return unmatched
def apply_filters(query, filters, gle):
if filters.get("company"):
query = query.where(gle.company == filters.company)
if filters.get("voucher_type"):
query = query.where(gle.voucher_type == filters.voucher_type)
if filters.get("from_date"):
query = query.where(gle.posting_date >= filters.from_date)
if filters.get("to_date"):
query = query.where(gle.posting_date <= filters.to_date)
return query

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

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

View File

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

View File

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

View File

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

View File

@ -115,7 +115,11 @@ def get_data(filters):
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(

View File

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

View File

@ -0,0 +1,56 @@
import os
import frappe
in_ci = os.environ.get("CI")
def execute():
try:
contacts = get_portal_user_contacts()
add_portal_users(contacts)
except Exception:
frappe.db.rollback()
frappe.log_error("Failed to migrate portal users")
if in_ci: # TODO: better way to handle this.
raise
def get_portal_user_contacts():
contact = frappe.qb.DocType("Contact")
dynamic_link = frappe.qb.DocType("Dynamic Link")
return (
frappe.qb.from_(contact)
.inner_join(dynamic_link)
.on(contact.name == dynamic_link.parent)
.select(
(dynamic_link.link_doctype).as_("doctype"),
(dynamic_link.link_name).as_("parent"),
(contact.email_id).as_("portal_user"),
)
.where(
(dynamic_link.parenttype == "Contact")
& (dynamic_link.link_doctype.isin(["Supplier", "Customer"]))
)
).run(as_dict=True)
def add_portal_users(contacts):
for contact in contacts:
user = frappe.db.get_value("User", {"email": contact.portal_user}, "name")
if not user:
continue
roles = frappe.get_roles(user)
required_role = contact.doctype
if required_role not in roles:
continue
portal_user_doc = frappe.new_doc("Portal User")
portal_user_doc.parenttype = contact.doctype
portal_user_doc.parentfield = "portal_users"
portal_user_doc.parent = contact.parent
portal_user_doc.user = user
portal_user_doc.insert()

View File

@ -55,6 +55,14 @@ frappe.ui.form.on("Supplier", {
}
};
});
frm.set_query("user", "portal_users", function(doc) {
return {
filters: {
"ignore_user_type": true,
}
};
});
},
refresh: function (frm) {

View File

@ -69,7 +69,10 @@
"on_hold",
"hold_type",
"column_break_59",
"release_date"
"release_date",
"portal_users_tab",
"portal_users",
"column_break_1mqv"
],
"fields": [
{
@ -451,6 +454,21 @@
"fieldname": "default_accounts_section",
"fieldtype": "Section Break",
"label": "Default Accounts"
},
{
"fieldname": "portal_users_tab",
"fieldtype": "Tab Break",
"label": "Portal Users"
},
{
"fieldname": "portal_users",
"fieldtype": "Table",
"label": "Supplier Portal Users",
"options": "Portal User"
},
{
"fieldname": "column_break_1mqv",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-user",
@ -463,7 +481,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-05-09 15:34:13.408932",
"modified": "2023-06-26 14:20:00.961554",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@ -16,6 +16,7 @@ from erpnext.accounts.party import ( # noqa
get_timeline_data,
validate_party_accounts,
)
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@ -46,12 +47,35 @@ class Supplier(TransactionBase):
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def on_update(self):
if not self.naming_series:
self.naming_series = ""
self.create_primary_contact()
self.create_primary_address()
def add_role_for_user(self):
for portal_user in self.portal_users:
add_role_for_portal_user(portal_user, "Supplier")
def _add_supplier_role(self, portal_user):
if not portal_user.is_new():
return
user_doc = frappe.get_doc("User", portal_user.user)
roles = {r.role for r in user_doc.roles}
if "Supplier" in roles:
return
if "System Manager" not in frappe.get_roles():
frappe.msgprint(
_("Please add 'Supplier' role to user {0}.").format(portal_user.user),
alert=True,
)
return
user_doc.add_roles("Supplier")
frappe.msgprint(
_("Added Supplier Role to User {0}.").format(frappe.bold(user_doc.name)), alert=True
)
def validate(self):
self.flags.is_new_doc = self.is_new()
@ -62,6 +86,7 @@ class Supplier(TransactionBase):
validate_party_accounts(self)
self.validate_internal_supplier()
self.add_role_for_user()
@frappe.whitelist()
def get_supplier_group_details(self):

View File

@ -7,6 +7,7 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.test_runner import make_test_records
from erpnext.accounts.party import get_due_date
from erpnext.controllers.website_list_for_contact import get_customers_suppliers
from erpnext.exceptions import PartyDisabled
test_dependencies = ["Payment Term", "Payment Terms Template"]
@ -195,6 +196,9 @@ class TestSupplier(FrappeTestCase):
def create_supplier(**args):
args = frappe._dict(args)
if not args.supplier_name:
args.supplier_name = frappe.generate_hash()
if frappe.db.exists("Supplier", args.supplier_name):
return frappe.get_doc("Supplier", args.supplier_name)
@ -209,3 +213,25 @@ def create_supplier(**args):
).insert()
return doc
class TestSupplierPortal(FrappeTestCase):
def test_portal_user_can_access_supplier_data(self):
supplier = create_supplier()
user = frappe.generate_hash() + "@example.com"
frappe.new_doc(
"User",
first_name="Supplier Portal User",
email=user,
send_welcome_email=False,
).insert()
supplier.append("portal_users", {"user": user})
supplier.save()
frappe.set_user(user)
_, suppliers = get_customers_suppliers("Purchase Order", user)
self.assertIn(supplier.name, suppliers)

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

@ -10,6 +10,7 @@ def set_print_templates_for_item_table(doc, settings):
doc.child_print_templates = {
"items": {
"qty": "templates/print_formats/includes/item_table_qty.html",
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
}
}

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

@ -232,22 +232,8 @@ def get_customers_suppliers(doctype, user):
has_supplier_field = meta.has_field("supplier")
if has_common(["Supplier", "Customer"], frappe.get_roles(user)):
contacts = frappe.db.sql(
"""
select
`tabContact`.email_id,
`tabDynamic Link`.link_doctype,
`tabDynamic Link`.link_name
from
`tabContact`, `tabDynamic Link`
where
`tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s
""",
user,
as_dict=1,
)
customers = [c.link_name for c in contacts if c.link_doctype == "Customer"]
suppliers = [c.link_name for c in contacts if c.link_doctype == "Supplier"]
suppliers = get_parents_for_user("Supplier")
customers = get_parents_for_user("Customer")
elif frappe.has_permission(doctype, "read", user=user):
customer_list = frappe.get_list("Customer")
customers = suppliers = [customer.name for customer in customer_list]
@ -255,6 +241,17 @@ def get_customers_suppliers(doctype, user):
return customers if has_customer_field else None, suppliers if has_supplier_field else None
def get_parents_for_user(parenttype: str) -> list[str]:
portal_user = frappe.qb.DocType("Portal User")
return (
frappe.qb.from_(portal_user)
.select(portal_user.parent)
.where(portal_user.user == frappe.session.user)
.where(portal_user.parenttype == parenttype)
).run(pluck="name")
def has_website_permission(doc, ptype, user, verbose=False):
doctype = doc.doctype
customers, suppliers = get_customers_suppliers(doctype, user)
@ -282,3 +279,28 @@ def get_customer_field_name(doctype):
return "party_name"
else:
return "customer"
def add_role_for_portal_user(portal_user, role):
"""When a new portal user is added, give appropriate roles to user if
posssible, else warn user to add roles."""
if not portal_user.is_new():
return
user_doc = frappe.get_doc("User", portal_user.user)
roles = {r.role for r in user_doc.roles}
if role in roles:
return
if "System Manager" not in frappe.get_roles():
frappe.msgprint(
_("Please add {1} role to user {0}.").format(portal_user.user, role),
alert=True,
)
return
user_doc.add_roles(role)
frappe.msgprint(
_("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True
)

View File

@ -74,7 +74,7 @@ class SocialMediaPost(Document):
def process_scheduled_social_media_posts():
posts = frappe.get_list(
posts = frappe.get_all(
"Social Media Post",
filters={"post_status": "Scheduled", "docstatus": 1},
fields=["name", "scheduled_time"],

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

@ -30,7 +30,7 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
"fieldname":"based_on_document",
"label": __("Based On Document"),
"fieldtype": "Select",
"options": ["Sales Order", "Delivery Note", "Quotation"],
"options": ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"],
"default": "Sales Order",
"reqd": 1
},

View File

@ -99,7 +99,9 @@ class ForecastingReport(ExponentialSmoothingForecast):
parent = frappe.qb.DocType(self.doctype)
child = frappe.qb.DocType(self.child_doctype)
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
date_field = (
"posting_date" if self.doctype in ("Delivery Note", "Sales Invoice") else "transaction_date"
)
query = (
frappe.qb.from_(parent)

View File

@ -339,3 +339,5 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapper', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True)
erpnext.patches.v14_0.cleanup_workspaces
erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users

View 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()

View File

@ -6,10 +6,14 @@ def execute():
assets = get_details_of_draft_or_submitted_depreciable_assets()
for asset in assets:
finance_book_rows = get_details_of_asset_finance_books_rows(asset.name)
asset_finance_books_map = get_asset_finance_books_map()
for fb_row in finance_book_rows:
asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
for asset in assets:
depreciation_schedules = asset_depreciation_schedules_map[asset.name]
for fb_row in asset_finance_books_map[asset.name]:
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row)
@ -19,7 +23,11 @@ def execute():
if asset.docstatus == 1:
asset_depr_schedule_doc.submit()
update_depreciation_schedules(asset.name, asset_depr_schedule_doc.name, fb_row.idx)
depreciation_schedules_of_fb_row = [
ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx)
]
update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name)
def get_details_of_draft_or_submitted_depreciable_assets():
@ -41,12 +49,33 @@ def get_details_of_draft_or_submitted_depreciable_assets():
return records
def get_details_of_asset_finance_books_rows(asset_name):
def group_records_by_asset_name(records):
grouped_dict = {}
for item in records:
key = list(item.keys())[0]
value = item[key]
if value not in grouped_dict:
grouped_dict[value] = []
del item["asset_name"]
grouped_dict[value].append(item)
return grouped_dict
def get_asset_finance_books_map():
afb = frappe.qb.DocType("Asset Finance Book")
asset = frappe.qb.DocType("Asset")
records = (
frappe.qb.from_(afb)
.join(asset)
.on(afb.parent == asset.name)
.select(
asset.name.as_("asset_name"),
afb.finance_book,
afb.idx,
afb.depreciation_method,
@ -55,23 +84,44 @@ def get_details_of_asset_finance_books_rows(asset_name):
afb.rate_of_depreciation,
afb.expected_value_after_useful_life,
)
.where(afb.parent == asset_name)
.where(asset.docstatus < 2)
.orderby(afb.idx)
).run(as_dict=True)
return records
asset_finance_books_map = group_records_by_asset_name(records)
return asset_finance_books_map
def update_depreciation_schedules(asset_name, asset_depr_schedule_name, fb_row_idx):
def get_asset_depreciation_schedules_map():
ds = frappe.qb.DocType("Depreciation Schedule")
asset = frappe.qb.DocType("Asset")
depr_schedules = (
records = (
frappe.qb.from_(ds)
.select(ds.name)
.where((ds.parent == asset_name) & (ds.finance_book_id == str(fb_row_idx)))
.join(asset)
.on(ds.parent == asset.name)
.select(
asset.name.as_("asset_name"),
ds.name,
ds.finance_book_id,
)
.where(asset.docstatus < 2)
.orderby(ds.idx)
).run(as_dict=True)
for idx, depr_schedule in enumerate(depr_schedules, start=1):
asset_depreciation_schedules_map = group_records_by_asset_name(records)
return asset_depreciation_schedules_map
def update_depreciation_schedules(
depreciation_schedules,
asset_depr_schedule_name,
):
ds = frappe.qb.DocType("Depreciation Schedule")
for idx, depr_schedule in enumerate(depreciation_schedules, start=1):
(
frappe.qb.update(ds)
.set(ds.idx, idx)

View File

@ -364,7 +364,8 @@
"default": "0",
"fieldname": "collect_progress",
"fieldtype": "Check",
"label": "Collect Progress"
"label": "Collect Progress",
"search_index": 1
},
{
"depends_on": "collect_progress",
@ -451,7 +452,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2023-04-17 21:11:11.346986",
"modified": "2023-06-28 18:57:11.603497",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",

View File

@ -1,355 +1,106 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "naming_series:",
"beta": 0,
"creation": "2018-01-18 09:44:47.565494",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "naming_series:",
"creation": "2018-01-18 09:44:47.565494",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"project",
"sent",
"column_break_2",
"date",
"time",
"section_break_5",
"users",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "naming_series",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Series",
"length": 0,
"no_copy": 0,
"options": "PROJ-UPD-.YYYY.-",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "naming_series",
"fieldtype": "Data",
"hidden": 1,
"label": "Series",
"options": "PROJ-UPD-.YYYY.-"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "project",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Project",
"length": 0,
"no_copy": 0,
"options": "Project",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "project",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Project",
"options": "Project",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "sent",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sent",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "0",
"fieldname": "sent",
"fieldtype": "Check",
"label": "Sent",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"read_only": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "users",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Users",
"length": 0,
"no_copy": 0,
"options": "Project User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "users",
"fieldtype": "Table",
"label": "Users",
"options": "Project User"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Project Update",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Project Update",
"print_hide": 1,
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-16 19:31:05.210656",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Update",
"name_case": "",
"owner": "Administrator",
],
"is_submittable": 1,
"links": [],
"modified": "2023-06-28 18:59:50.678917",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Update",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects User",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

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

@ -76,6 +76,14 @@ frappe.ui.form.on("Customer", {
}
}
});
frm.set_query("user", "portal_users", function() {
return {
filters: {
"ignore_user_type": true,
}
};
});
},
customer_primary_address: function(frm){
if(frm.doc.customer_primary_address){

View File

@ -81,7 +81,9 @@
"dn_required",
"column_break_53",
"is_frozen",
"disabled"
"disabled",
"portal_users_tab",
"portal_users"
],
"fields": [
{
@ -555,6 +557,17 @@
{
"fieldname": "column_break_54",
"fieldtype": "Column Break"
},
{
"fieldname": "portal_users_tab",
"fieldtype": "Tab Break",
"label": "Portal Users"
},
{
"fieldname": "portal_users",
"fieldtype": "Table",
"label": "Customer Portal Users",
"options": "Portal User"
}
],
"icon": "fa fa-user",
@ -568,7 +581,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-05-09 15:38:40.255193",
"modified": "2023-06-22 13:21:10.678382",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@ -22,6 +22,7 @@ from erpnext.accounts.party import ( # noqa
get_timeline_data,
validate_party_accounts,
)
from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
from erpnext.utilities.transaction_base import TransactionBase
@ -82,6 +83,7 @@ class Customer(TransactionBase):
self.check_customer_group_change()
self.validate_default_bank_account()
self.validate_internal_customer()
self.add_role_for_user()
# set loyalty program tier
if frappe.db.exists("Customer", self.name):
@ -170,6 +172,10 @@ class Customer(TransactionBase):
self.update_customer_groups()
def add_role_for_user(self):
for portal_user in self.portal_users:
add_role_for_portal_user(portal_user, "Customer")
def update_customer_groups(self):
ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"]
if frappe.flags.customer_group_changed:

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

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

View File

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

View File

@ -590,7 +590,7 @@ $.extend(erpnext.item, {
let selected_attributes = {};
me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
if(i===0) return;
let attribute_name = $(col).find('.control-label').html().trim();
let attribute_name = $(col).find('.column-label').html().trim();
selected_attributes[attribute_name] = [];
let checked_opts = $(col).find('.checkbox input');
checked_opts.each((i, opt) => {

View File

@ -81,7 +81,7 @@
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@ -147,7 +147,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-07-28 19:15:38.270046",
"modified": "2023-06-21 15:13:38.270046",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Reorder",
@ -158,4 +158,4 @@
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
}
}

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

@ -72,6 +72,11 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
def test_make_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
create_payment_term("_Test Payment Term 1 for Purchase Invoice")
create_payment_term("_Test Payment Term 2 for Purchase Invoice")
if not frappe.db.exists(
"Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
):
@ -83,12 +88,14 @@ class TestPurchaseReceipt(FrappeTestCase):
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 1 for Purchase Invoice",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 00,
},
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 2 for Purchase Invoice",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 30,
@ -1827,6 +1834,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

@ -13,7 +13,7 @@ frappe.ui.form.on("Warehouse", {
};
});
frm.set_query("parent_warehouse", function () {
frm.set_query("parent_warehouse", function (doc) {
return {
filters: {
is_group: 1,

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

@ -67,7 +67,7 @@ def _reorder_item():
else:
projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
if (reorder_level or reorder_qty) and projected_qty < reorder_level:
if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
deficiency = reorder_level - projected_qty
if deficiency > reorder_qty:
reorder_qty = deficiency

View File

@ -312,7 +312,35 @@ def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
def get_serial_or_batch_nos(bundle):
return frappe.get_all("Serial and Batch Entry", fields=["*"], filters={"parent": bundle})
# For print format
bundle_data = frappe.get_cached_value(
"Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True
)
fields = []
if bundle_data.has_serial_no:
fields.append("serial_no")
if bundle_data.has_batch_no:
fields.extend(["batch_no", "qty"])
data = frappe.get_all("Serial and Batch Entry", fields=fields, filters={"parent": bundle})
if bundle_data.has_serial_no and not bundle_data.has_batch_no:
return ", ".join([d.serial_no for d in data])
elif bundle_data.has_batch_no:
html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
for d in data:
if d.serial_no:
html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th ><th>{abs(d.qty)}</th></tr>"
else:
html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
html += "</table>"
return html
class SerialNoValuation(DeprecatedSerialNoValuation):

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

Some files were not shown because too many files have changed in this diff Show More