Merge branch 'develop' into split-invoice-refactor

This commit is contained in:
Marica 2023-11-09 17:28:01 +05:30 committed by GitHub
commit a27374fd8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
180 changed files with 2361 additions and 1127 deletions

View File

@ -112,7 +112,8 @@ class AutoMatchbyPartyNameDescription:
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
field = party.lower() + "_name"
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
for field in ["bank_party_name", "description"]:
if not self.get(field):
@ -131,7 +132,11 @@ class AutoMatchbyPartyNameDescription:
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
skip = False
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
result = process.extract(
query=self.get(field),
choices={row.get("name"): row.get("party_name") for row in names},
scorer=fuzz.token_set_ratio,
)
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
@ -149,14 +154,14 @@ class AutoMatchbyPartyNameDescription:
Returns: Result, Skip (whether or not to discontinue matching)
"""
PARTY, SCORE, CUTOFF = 0, 1, 80
SCORE, PARTY_ID, CUTOFF = 1, 2, 80
if not result or not len(result):
return None, False
first_result = result[0]
if len(result) == 1:
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
@ -165,7 +170,7 @@ class AutoMatchbyPartyNameDescription:
if first_result[SCORE] == second_result[SCORE]:
return None, True
return first_result[PARTY], True
return first_result[PARTY_ID], True
else:
return None, False

View File

@ -154,7 +154,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) {
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
}, __('Actions'));

View File

@ -595,6 +595,7 @@
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nSubmitted\nCancelled",
"read_only": 1
},
@ -750,7 +751,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-06-23 18:07:38.023010",
"modified": "2023-11-08 21:51:03.482709",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
get_account_currency,
get_balance_on,
get_outstanding_invoices,
get_party_types_from_account_type,
)
from erpnext.controllers.accounts_controller import (
AccountsController,
@ -83,7 +84,6 @@ class PaymentEntry(AccountsController):
self.apply_taxes()
self.set_amounts_after_tax()
self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice()
self.validate_transaction_reference()
self.set_title()
self.set_remarks()
@ -952,35 +952,6 @@ class PaymentEntry(AccountsController):
self.name,
)
def validate_payment_against_negative_invoice(self):
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type != "Receive" or self.party_type != "Supplier"
):
return
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)
if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
frappe.throw(msg, InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
fmt_money(total_negative_outstanding)
),
InvalidPaymentEntry,
)
def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
@ -1083,9 +1054,7 @@ class PaymentEntry(AccountsController):
item=self,
)
dr_or_cr = (
"credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit"
)
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
for d in self.get("references"):
cost_center = self.cost_center
@ -1103,10 +1072,27 @@ class PaymentEntry(AccountsController):
against_voucher_type = d.reference_doctype
against_voucher = d.reference_name
reverse_dr_or_cr, standalone_note = 0, 0
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
is_return, return_against = frappe.db.get_value(
d.reference_doctype, d.reference_name, ["is_return", "return_against"]
)
payable_party_types = get_party_types_from_account_type("Payable")
receivable_party_types = get_party_types_from_account_type("Receivable")
if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"):
reverse_dr_or_cr = 1
elif (
is_return and self.party_type in payable_party_types and (self.payment_type == "Receive")
):
reverse_dr_or_cr = 1
if is_return and not return_against and not reverse_dr_or_cr:
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update(
{
dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr + "_in_account_currency": d.allocated_amount,
dr_or_cr: abs(allocated_amount_in_company_currency),
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
"against_voucher_type": against_voucher_type,
"against_voucher": against_voucher,
"cost_center": cost_center,

View File

@ -684,17 +684,6 @@ class TestPaymentEntry(FrappeTestCase):
self.validate_gl_entries(pe.name, expected_gle)
def test_payment_against_negative_sales_invoice(self):
pe1 = frappe.new_doc("Payment Entry")
pe1.payment_type = "Pay"
pe1.company = "_Test Company"
pe1.party_type = "Customer"
pe1.party = "_Test Customer"
pe1.paid_from = "_Test Cash - _TC"
pe1.paid_amount = 100
pe1.received_amount = 100
self.assertRaises(InvalidPaymentEntry, pe1.validate)
si1 = create_sales_invoice()
# create full payment entry against si1
@ -752,8 +741,6 @@ class TestPaymentEntry(FrappeTestCase):
# pay more than outstanding against si1
pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC")
pe3.paid_amount = pe3.received_amount = 300
self.assertRaises(InvalidPaymentEntry, pe3.validate)
# pay negative outstanding against si1
pe3.paid_to = "Debtors - _TC"
@ -1301,6 +1288,39 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(references[2].voucher_no, si2.name)
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
self.assertEqual(references[2].payment_term, "Tax Receivable")
def test_receive_payment_from_payable_party_type(self):
pe = create_payment_entry(
party_type="Supplier",
party="_Test Supplier",
payment_type="Receive",
paid_from="Creditors - _TC",
paid_to="_Test Cash - _TC",
save=True,
submit=True,
)
self.voucher_no = pe.name
self.expected_gle = [
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
{"account": "Creditors - _TC", "debit": 0.0, "credit": 1000.0},
]
self.check_gl_entries()
def check_gl_entries(self):
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(
gle.account,
gle.debit,
gle.credit,
)
.where((gle.voucher_no == self.voucher_no) & (gle.is_cancelled == 0))
.orderby(gle.account)
).run(as_dict=True)
for row in range(len(self.expected_gle)):
for field in ["account", "debit", "credit"]:
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
def create_payment_entry(**args):

View File

@ -65,7 +65,8 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Voucher Type",
"options": "DocType"
"options": "DocType",
"search_index": 1
},
{
"fieldname": "voucher_no",
@ -73,14 +74,16 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"options": "voucher_type"
"options": "voucher_type",
"search_index": 1
},
{
"fieldname": "against_voucher_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Against Voucher Type",
"options": "DocType"
"options": "DocType",
"search_index": 1
},
{
"fieldname": "against_voucher_no",
@ -88,7 +91,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Against Voucher No",
"options": "against_voucher_type"
"options": "against_voucher_type",
"search_index": 1
},
{
"fieldname": "amount",
@ -148,13 +152,14 @@
{
"fieldname": "voucher_detail_no",
"fieldtype": "Data",
"label": "Voucher Detail No"
"label": "Voucher Detail No",
"search_index": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-30 16:15:00.470283",
"modified": "2023-11-03 16:39:58.904113",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Ledger Entry",

View File

@ -109,6 +109,8 @@ class PaymentReconciliation(Document):
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
# nosemgrep
journal_entries = frappe.db.sql(
"""
@ -132,11 +134,13 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition}
END)
order by t1.posting_date
{limit}
""".format(
**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"condition": condition,
"limit": limit,
}
),
{
@ -162,7 +166,7 @@ class PaymentReconciliation(Document):
if self.payment_name:
conditions.append(doc.name.like(f"%{self.payment_name}%"))
self.return_invoices = (
self.return_invoices_query = (
qb.from_(doc)
.select(
ConstantColumn(voucher_type).as_("voucher_type"),
@ -170,8 +174,11 @@ class PaymentReconciliation(Document):
doc.return_against,
)
.where(Criterion.all(conditions))
.run(as_dict=True)
)
if self.payment_limit:
self.return_invoices_query = self.return_invoices_query.limit(self.payment_limit)
self.return_invoices = self.return_invoices_query.run(as_dict=True)
def get_dr_or_cr_notes(self):

View File

@ -6,7 +6,6 @@ import unittest
import frappe
from frappe import _
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
@ -126,64 +125,70 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(inv.grand_total, 5474.0)
def test_tax_calculation_with_item_tax_template(self):
import json
inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1)
item_row = inv.get("items")[0]
from erpnext.stock.get_item_details import get_item_details
# set tax template in item
item = frappe.get_cached_doc("Item", "_Test Item")
item.set(
"taxes",
[
{
"item_tax_template": "_Test Account Excise Duty @ 15 - _TC",
"valid_from": add_days(nowdate(), -5),
}
],
)
item.save()
# create POS invoice with item
pos_inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True)
item_details = get_item_details(
doc=pos_inv,
args={
"item_code": item.item_code,
"company": pos_inv.company,
"doctype": "POS Invoice",
"conversion_rate": 1.0,
},
)
tax_map = json.loads(item_details.item_tax_rate)
for tax in tax_map:
pos_inv.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": tax,
"rate": tax_map[tax],
"description": "Test",
"cost_center": "_Test Cost Center - _TC",
},
)
pos_inv.submit()
pos_inv.load_from_db()
# check if correct tax values are applied from tax template
self.assertEqual(pos_inv.net_total, 386.4)
expected_taxes = [
{
"tax_amount": 57.96,
"total": 444.36,
},
add_items = [
(54, "_Test Account Excise Duty @ 12 - _TC"),
(288, "_Test Account Excise Duty @ 15 - _TC"),
(144, "_Test Account Excise Duty @ 20 - _TC"),
(430, "_Test Item Tax Template 1 - _TC"),
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
item_row_copy.qty = qty
item_row_copy.item_tax_template = item_tax_template
inv.append("items", item_row_copy)
for i in range(len(expected_taxes)):
for key in expected_taxes[i]:
self.assertEqual(expected_taxes[i][key], pos_inv.get("taxes")[i].get(key))
inv.append(
"taxes",
{
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Sales Taxes and Charges",
"rate": 11,
},
)
inv.append(
"taxes",
{
"account_head": "_Test Account Education Cess - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 0,
},
)
inv.append(
"taxes",
{
"account_head": "_Test Account S&H Education Cess - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "S&H Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 3,
},
)
inv.insert()
self.assertEqual(pos_inv.get("base_total_taxes_and_charges"), 57.96)
self.assertEqual(inv.net_total, 4600)
self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41)
self.assertEqual(inv.get("taxes")[0].total, 5102.41)
self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80)
self.assertEqual(inv.get("taxes")[1].total, 5300.21)
self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36)
self.assertEqual(inv.get("taxes")[2].total, 5675.57)
self.assertEqual(inv.grand_total, 5675.57)
self.assertEqual(inv.rounding_adjustment, 0.43)
self.assertEqual(inv.rounded_total, 5676.0)
def test_tax_calculation_with_multiple_items_and_discount(self):
inv = create_pos_invoice(qty=1, rate=75, do_not_save=True)

View File

@ -110,7 +110,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-04-21 17:36:26.642617",
"modified": "2023-11-02 11:32:12.254018",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",
@ -125,7 +125,19 @@
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}

View File

@ -384,7 +384,8 @@
"label": "Supplier Invoice No",
"oldfieldname": "bill_no",
"oldfieldtype": "Data",
"print_hide": 1
"print_hide": 1,
"search_index": 1
},
{
"fieldname": "column_break_15",
@ -407,7 +408,8 @@
"no_copy": 1,
"options": "Purchase Invoice",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "section_addresses",
@ -1602,7 +1604,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-10-16 16:24:51.886231",
"modified": "2023-11-03 15:47:30.319200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@ -1665,4 +1667,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}
}

View File

@ -1783,9 +1783,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
set_advance_flag(company="_Test Company", flag=0, default_account="")
def test_gl_entries_for_standalone_debit_note(self):
make_purchase_invoice(qty=5, rate=500, update_stock=True)
from erpnext.stock.doctype.item.test_item import make_item
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
item_code = make_item(properties={"is_stock_item": 1})
make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True)
returned_inv = make_purchase_invoice(
item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True
)
# override the rate with valuation rate
sle = frappe.get_all(
@ -1795,7 +1800,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)[0]
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
self.assertAlmostEqual(rate, 500)
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
@ -1898,6 +1903,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
disable_dimension()
def test_repost_accounting_entries(self):
# update repost settings
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
settings.save()
pi = make_purchase_invoice(
rate=1000,
price_list_rate=1000,

View File

@ -5,9 +5,7 @@ frappe.ui.form.on("Repost Accounting Ledger", {
setup: function(frm) {
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
return {
filters: {
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
}
query: "erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.get_repost_allowed_types"
}
}

View File

@ -10,9 +10,12 @@ from frappe.utils.data import comma_and
class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs):
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
self._allowed_types = set(
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
)
self._allowed_types = [
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
)
]
def validate(self):
self.validate_vouchers()
@ -157,7 +160,7 @@ def start_repost(account_repost_doc=str) -> None:
doc.docstatus = 1
doc.make_gl_entries()
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)
doc.make_gl_entries()
@ -186,3 +189,18 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
)
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
filters = {"allowed": True}
if txt:
filters.update({"document_type": ("like", f"%{txt}%")})
if allowed_types := frappe.db.get_all(
"Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1
):
return allowed_types
return []

View File

@ -20,10 +20,18 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company()
self.create_customer()
self.create_item()
self.update_repost_settings()
def teadDown(self):
frappe.db.rollback()
def update_repost_settings(self):
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
def test_01_basic_functions(self):
si = create_sales_invoice(
item=self.item,

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Repost Accounting Ledger Settings", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,46 @@
{
"actions": [],
"creation": "2023-11-07 09:57:20.619939",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"allowed_types"
],
"fields": [
{
"fieldname": "allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"in_create": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-07 14:24:13.321522",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "System Manager",
"select": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostAccountingLedgerSettings(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestRepostAccountingLedgerSettings(FrappeTestCase):
pass

View File

@ -0,0 +1,45 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-11-07 09:58:03.595382",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"column_break_sfzb",
"allowed"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Doctype",
"options": "DocType"
},
{
"default": "0",
"fieldname": "allowed",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allowed"
},
{
"fieldname": "column_break_sfzb",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-07 10:01:39.217861",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Allowed Types",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RepostAllowedTypes(Document):
pass

View File

@ -187,7 +187,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
@ -563,15 +562,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
}
}
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {
return{
query: "erpnext.controllers.queries.get_income_account",
filters: {'company': doc.company}
}
});
// Cost Center in Details Table
// -----------------------------
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) {
@ -666,6 +656,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("income_account", "items", function() {
return{
query: "erpnext.controllers.queries.get_income_account",
filters: {
'company': frm.doc.company,
"disabled": 0
}
}
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',

View File

@ -26,6 +26,7 @@
"is_return",
"return_against",
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"is_debit_note",
"amended_from",
"accounting_dimensions_section",
@ -2153,6 +2154,13 @@
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company default Cost Center for Round off"
},
{
"default": "0",
"depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
}
],
"icon": "fa fa-file-text",
@ -2165,7 +2173,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-07-25 16:02:18.988799",
"modified": "2023-11-03 14:39:38.012346",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -253,6 +253,7 @@ class SalesInvoice(SellingController):
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_billing_status_in_dn()
self.clear_unallocated_mode_of_payments()
@ -1019,7 +1020,7 @@ class SalesInvoice(SellingController):
def make_customer_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
# because rounded_total had value even before introduction of posting GLE based on rounded total
grand_total = (
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
)
@ -1267,7 +1268,7 @@ class SalesInvoice(SellingController):
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
if payment_mode.amount:
if payment_mode.base_amount:
# POS, make payment entries
gl_entries.append(
self.get_gl_dict(
@ -1429,6 +1430,8 @@ class SalesInvoice(SellingController):
)
def update_billing_status_in_dn(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_delivery_note:
return
updated_delivery_notes = []
for d in self.get("items"):
if d.dn_detail:

View File

@ -516,72 +516,70 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.grand_total, 5474.0)
def test_tax_calculation_with_item_tax_template(self):
import json
from erpnext.stock.get_item_details import get_item_details
# set tax template in item
item = frappe.get_cached_doc("Item", "_Test Item")
item.set(
"taxes",
[
{
"item_tax_template": "_Test Item Tax Template 1 - _TC",
"valid_from": add_days(nowdate(), -5),
}
],
)
item.save()
# create sales invoice with item
si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True)
item_details = get_item_details(
doc=si,
args={
"item_code": item.item_code,
"company": si.company,
"doctype": "Sales Invoice",
"conversion_rate": 1.0,
item_row = si.get("items")[0]
add_items = [
(54, "_Test Account Excise Duty @ 12 - _TC"),
(288, "_Test Account Excise Duty @ 15 - _TC"),
(144, "_Test Account Excise Duty @ 20 - _TC"),
(430, "_Test Item Tax Template 1 - _TC"),
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
item_row_copy.qty = qty
item_row_copy.item_tax_template = item_tax_template
si.append("items", item_row_copy)
si.append(
"taxes",
{
"account_head": "_Test Account Excise Duty - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"doctype": "Sales Taxes and Charges",
"rate": 11,
},
)
tax_map = json.loads(item_details.item_tax_rate)
for tax in tax_map:
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": tax,
"rate": tax_map[tax],
"description": "Test",
"cost_center": "_Test Cost Center - _TC",
},
)
si.submit()
si.load_from_db()
# check if correct tax values are applied from tax template
self.assertEqual(si.net_total, 386.4)
expected_taxes = [
si.append(
"taxes",
{
"tax_amount": 19.32,
"total": 405.72,
"account_head": "_Test Account Education Cess - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 0,
},
)
si.append(
"taxes",
{
"tax_amount": 38.64,
"total": 444.36,
"account_head": "_Test Account S&H Education Cess - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "S&H Education Cess",
"doctype": "Sales Taxes and Charges",
"rate": 3,
},
{
"tax_amount": 57.96,
"total": 502.32,
},
]
)
si.insert()
for i in range(len(expected_taxes)):
for key in expected_taxes[i]:
self.assertEqual(expected_taxes[i][key], si.get("taxes")[i].get(key))
self.assertEqual(si.net_total, 4600)
self.assertEqual(si.get("base_total_taxes_and_charges"), 115.92)
self.assertEqual(si.get("taxes")[0].tax_amount, 502.41)
self.assertEqual(si.get("taxes")[0].total, 5102.41)
self.assertEqual(si.get("taxes")[1].tax_amount, 197.80)
self.assertEqual(si.get("taxes")[1].total, 5300.21)
self.assertEqual(si.get("taxes")[2].tax_amount, 375.36)
self.assertEqual(si.get("taxes")[2].total, 5675.57)
self.assertEqual(si.grand_total, 5675.57)
self.assertEqual(si.rounding_adjustment, 0.43)
self.assertEqual(si.rounded_total, 5676.0)
def test_tax_calculation_with_multiple_items_and_discount(self):
si = create_sales_invoice(qty=1, rate=75, do_not_save=True)

View File

@ -143,7 +143,18 @@ frappe.query_reports["Accounts Payable"] = {
"fieldname": "show_future_payments",
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
},
{
"fieldname": "ignore_accounts",
"label": __("Group by Voucher"),
"fieldtype": "Check",
}
],
"formatter": function(value, row, column, data, default_formatter) {
@ -175,4 +186,4 @@ function get_party_type_options() {
});
});
return options;
}
}

View File

@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60,
"range3": 90,
"range4": 120,
"in_party_currency": 1,
}
data = execute(filters)

View File

@ -114,10 +114,13 @@ frappe.query_reports["Accounts Receivable"] = {
"reqd": 1
},
{
"fieldname": "customer_group",
"fieldname":"customer_group",
"label": __("Customer Group"),
"fieldtype": "Link",
"options": "Customer Group"
"fieldtype": "MultiSelectList",
"options": "Customer Group",
get_data: function(txt) {
return frappe.db.get_link_options('Customer Group', txt);
}
},
{
"fieldname": "payment_terms_template",
@ -172,7 +175,18 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
},
{
"fieldname": "ignore_accounts",
"label": __("Group by Voucher"),
"fieldtype": "Check",
}
],
"formatter": function(value, row, column, data, default_formatter) {
@ -205,4 +219,4 @@ function get_party_type_options() {
});
});
return options;
}
}

View File

@ -14,7 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.utils import get_currency_precision
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
# This report gives a summary of all Outstanding Invoices considering the following
@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
# 7. For overpayment against an invoice with payment terms, there will be an additional row
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable"
# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
# 10. This report is based on Payment Ledger Entries
def execute(filters=None):
@ -72,9 +72,7 @@ class ReceivablePayableReport(object):
self.currency_precision = get_currency_precision() or 2
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
self.account_type = self.filters.account_type
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_type = get_party_types_from_account_type(self.account_type)
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
@ -84,6 +82,9 @@ class ReceivablePayableReport(object):
self.total_row_map = {}
self.skip_total_row = 1
if self.filters.get("in_party_currency"):
self.skip_total_row = 1
def get_data(self):
self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person()
@ -116,7 +117,12 @@ class ReceivablePayableReport(object):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party)
else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if not key in self.voucher_balance:
self.voucher_balance[key] = frappe._dict(
voucher_type=ple.voucher_type,
@ -140,7 +146,7 @@ class ReceivablePayableReport(object):
if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"):
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
self.init_subtotal_row("Total")
def get_invoices(self, ple):
@ -183,7 +189,10 @@ class ReceivablePayableReport(object):
):
return
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
if self.filters.get("ignore_accounts"):
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
else:
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
# If payment is made against credit note
# and credit note is made against a Sales Invoice
@ -192,13 +201,19 @@ class ReceivablePayableReport(object):
if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no)
if return_against:
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
if self.filters.get("ignore_accounts"):
key = (ple.against_voucher_type, return_against, ple.party)
else:
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
row = self.voucher_balance.get(key)
if not row:
# no invoice, this is an invoice / stand-alone payment / credit note
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
if self.filters.get("ignore_accounts"):
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
else:
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
row.party_type = ple.party_type
return row
@ -210,8 +225,7 @@ class ReceivablePayableReport(object):
if not row:
return
# amount in "Party Currency", if its supplied. If not, amount in company currency
if self.filters.get("party_type") and self.filters.get("party"):
if self.filters.get("in_party_currency"):
amount = ple.amount_in_account_currency
else:
amount = ple.amount
@ -242,8 +256,10 @@ class ReceivablePayableReport(object):
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
for field in self.get_currency_fields():
total_row[field] += row.get(field, 0.0)
if total_row:
for field in self.get_currency_fields():
total_row[field] += row.get(field, 0.0)
total_row["currency"] = row.get("currency", "")
def append_subtotal_row(self, party):
sub_total_row = self.total_row_map.get(party)
@ -295,7 +311,7 @@ class ReceivablePayableReport(object):
if self.filters.get("group_by_party"):
self.append_subtotal_row(self.previous_party)
if self.data:
self.data.append(self.total_row_map.get("Total"))
self.data.append(self.total_row_map.get("Total", {}))
def append_row(self, row):
self.allocate_future_payments(row)
@ -426,7 +442,7 @@ class ReceivablePayableReport(object):
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
if self.filters.get("party_type") and self.filters.get("party"):
if self.filters.get("in_party_currency"):
row.currency = row.account_currency
else:
row.currency = self.company_currency
@ -718,6 +734,7 @@ class ReceivablePayableReport(object):
query = (
qb.from_(ple)
.select(
ple.name,
ple.account,
ple.voucher_type,
ple.voucher_no,
@ -731,13 +748,15 @@ class ReceivablePayableReport(object):
ple.account_currency,
ple.amount,
ple.amount_in_account_currency,
ple.remarks,
)
.where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter))
.where(Criterion.any(self.or_filters))
)
if self.filters.get("show_remarks"):
query = query.select(ple.remarks)
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
@ -823,7 +842,13 @@ class ReceivablePayableReport(object):
self.customer = qb.DocType("Customer")
if self.filters.get("customer_group"):
self.get_hierarchical_filters("Customer Group", "customer_group")
groups = get_customer_group_with_children(self.filters.customer_group)
customers = (
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer["customer_group"].isin(groups))
)
self.qb_selection_filter.append(self.ple.party.isin(customers))
if self.filters.get("territory"):
self.get_hierarchical_filters("Territory", "territory")
@ -1115,3 +1140,19 @@ class ReceivablePayableReport(object):
.run()
)
self.err_journals = [x[0] for x in results] if results else []
def get_customer_group_with_children(customer_groups):
if not isinstance(customer_groups, list):
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
all_customer_groups = []
for d in customer_groups:
if frappe.db.exists("Customer Group", d):
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_customer_groups += [c.name for c in children]
else:
frappe.throw(_("Customer Group: {0} does not exist").format(d))
return list(set(all_customer_groups))

View File

@ -475,6 +475,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1]
self.assertEqual(len(report), 0)
def test_multi_customer_group_filter(self):
si = self.create_sales_invoice()
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
# Create a list of customer groups, e.g., ["Group1", "Group2"]
cus_groups_list = [cus_group, "_Test Customer Group 1"]
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer_group": cus_groups_list, # Use the list of customer groups
}
report = execute(filters)[1]
# Assert that the report contains data for the specified customer groups
self.assertTrue(len(report) > 0)
for row in report:
# Assert that the customer group of each row is in the list of customer groups
self.assertIn(row.customer_group, cus_groups_list)
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (
@ -557,6 +581,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60,
"range3": 90,
"range4": 120,
"in_party_currency": 1,
}
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)

View File

@ -8,6 +8,7 @@ from frappe.utils import cint, flt
from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
from erpnext.accounts.utils import get_party_types_from_account_type
def execute(filters=None):
@ -22,9 +23,7 @@ def execute(filters=None):
class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args):
self.account_type = args.get("account_type")
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_type = get_party_types_from_account_type(self.account_type)
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
)

View File

@ -31,6 +31,18 @@ frappe.query_reports["Asset Depreciation Ledger"] = {
"fieldtype": "Link",
"options": "Asset"
},
{
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
"options": "Asset Category"
},
{
"fieldname":"cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center"
},
{
"fieldname":"finance_book",
"label": __("Finance Book"),
@ -38,10 +50,10 @@ frappe.query_reports["Asset Depreciation Ledger"] = {
"options": "Finance Book"
},
{
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
"options": "Asset Category"
}
"fieldname": "include_default_book_assets",
"label": __("Include Default FB Assets"),
"fieldtype": "Check",
"default": 1
},
]
}

View File

@ -1,15 +1,15 @@
{
"add_total_row": 1,
"add_total_row": 0,
"columns": [],
"creation": "2016-04-08 14:49:58.133098",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"idx": 6,
"is_standard": "Yes",
"letterhead": null,
"modified": "2023-07-26 21:05:33.554778",
"modified": "2023-11-08 20:17:05.774211",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciation Ledger",

View File

@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils import cstr, flt
def execute(filters=None):
@ -32,7 +32,6 @@ def get_data(filters):
filters_data.append(["against_voucher", "=", filters.get("asset")])
if filters.get("asset_category"):
assets = frappe.db.sql_list(
"""select name from tabAsset
where asset_category = %s and docstatus=1""",
@ -41,12 +40,27 @@ def get_data(filters):
filters_data.append(["against_voucher", "in", assets])
if filters.get("finance_book"):
filters_data.append(["finance_book", "in", ["", filters.get("finance_book")]])
company_fb = frappe.get_cached_value("Company", filters.get("company"), "default_finance_book")
if filters.get("include_default_book_assets") and company_fb:
if filters.get("finance_book") and cstr(filters.get("finance_book")) != cstr(company_fb):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Assets'"))
else:
finance_book = company_fb
elif filters.get("finance_book"):
finance_book = filters.get("finance_book")
else:
finance_book = None
if finance_book:
or_filters_data = [["finance_book", "in", ["", finance_book]], ["finance_book", "is", "not set"]]
else:
or_filters_data = [["finance_book", "in", [""]], ["finance_book", "is", "not set"]]
gl_entries = frappe.get_all(
"GL Entry",
filters=filters_data,
or_filters=or_filters_data,
fields=["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"],
order_by="against_voucher, posting_date",
)
@ -61,7 +75,9 @@ def get_data(filters):
asset_data = assets_details.get(d.against_voucher)
if asset_data:
if not asset_data.get("accumulated_depreciation_amount"):
asset_data.accumulated_depreciation_amount = d.debit
asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
"opening_accumulated_depreciation"
)
else:
asset_data.accumulated_depreciation_amount += d.debit
@ -70,7 +86,7 @@ def get_data(filters):
{
"depreciation_amount": d.debit,
"depreciation_date": d.posting_date,
"amount_after_depreciation": (
"value_after_depreciation": (
flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount)
),
"depreciation_entry": d.voucher_no,
@ -88,10 +104,12 @@ def get_assets_details(assets):
fields = [
"name as asset",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"asset_category",
"status",
"depreciation_method",
"purchase_date",
"cost_center",
]
for d in frappe.get_all("Asset", fields=fields, filters={"name": ("in", assets)}):
@ -121,6 +139,12 @@ def get_columns():
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Opening Accumulated Depreciation"),
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Depreciation Amount"),
"fieldname": "depreciation_amount",
@ -134,8 +158,8 @@ def get_columns():
"width": 210,
},
{
"label": _("Amount After Depreciation"),
"fieldname": "amount_after_depreciation",
"label": _("Value After Depreciation"),
"fieldname": "value_after_depreciation",
"fieldtype": "Currency",
"width": 180,
},
@ -153,12 +177,13 @@ def get_columns():
"options": "Asset Category",
"width": 120,
},
{"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120},
{
"label": _("Depreciation Method"),
"fieldname": "depreciation_method",
"fieldtype": "Data",
"width": 130,
"label": _("Cost Center"),
"fieldtype": "Link",
"fieldname": "cost_center",
"options": "Cost Center",
"width": 100,
},
{"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120},
{"label": _("Purchase Date"), "fieldname": "purchase_date", "fieldtype": "Date", "width": 120},
]

View File

@ -17,7 +17,7 @@ frappe.query_reports["Balance Sheet"]["filters"].push({
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default Book Entries"),
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});

View File

@ -17,7 +17,7 @@ frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
frappe.query_reports["Cash Flow"]["filters"].push(
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"label": __("Include Default FB Entries"),
"fieldtype": "Check",
"default": 1
}

View File

@ -104,7 +104,7 @@ frappe.query_reports["Consolidated Financial Statement"] = {
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"label": __("Include Default FB Entries"),
"fieldtype": "Check",
"default": 1
},

View File

@ -561,9 +561,7 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
query = query.where(
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@ -79,7 +79,9 @@ class General_Payment_Ledger_Comparison(object):
.select(
gle.company,
gle.account,
gle.voucher_type,
gle.voucher_no,
gle.party_type,
gle.party,
outstanding,
)
@ -89,7 +91,9 @@ class General_Payment_Ledger_Comparison(object):
& (gle.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
.groupby(
gle.company, gle.account, gle.voucher_type, gle.voucher_no, gle.party_type, gle.party
)
.run()
)
@ -112,7 +116,13 @@ class General_Payment_Ledger_Comparison(object):
self.account_types[acc_type].ple = (
qb.from_(ple)
.select(
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
ple.company,
ple.account,
ple.voucher_type,
ple.voucher_no,
ple.party_type,
ple.party,
Sum(ple.amount).as_("outstanding"),
)
.where(
(ple.company == self.filters.company)
@ -120,7 +130,9 @@ class General_Payment_Ledger_Comparison(object):
& (ple.account.isin(val.accounts))
)
.where(Criterion.all(filter_criterion))
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
.groupby(
ple.company, ple.account, ple.voucher_type, ple.voucher_no, ple.party_type, ple.party
)
.run()
)
@ -138,12 +150,12 @@ class General_Payment_Ledger_Comparison(object):
self.diff = frappe._dict({})
for x in self.variation_in_payment_ledger:
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
self.diff[(x[0], x[1], x[2], x[3], x[4], x[5])] = frappe._dict({"gl_balance": x[6]})
for x in self.variation_in_general_ledger:
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
frappe._dict({"pl_balance": x[4]})
)
self.diff.setdefault(
(x[0], x[1], x[2], x[3], x[4], x[5]), frappe._dict({"gl_balance": 0.0})
).update(frappe._dict({"pl_balance": x[6]}))
def generate_data(self):
self.data = []
@ -151,8 +163,12 @@ class General_Payment_Ledger_Comparison(object):
self.data.append(
frappe._dict(
{
"voucher_no": key[2],
"party": key[3],
"company": key[0],
"account": key[1],
"voucher_type": key[2],
"voucher_no": key[3],
"party_type": key[4],
"party": key[5],
"gl_balance": val.gl_balance,
"pl_balance": val.pl_balance,
}
@ -162,12 +178,52 @@ class General_Payment_Ledger_Comparison(object):
def get_columns(self):
self.columns = []
options = None
self.columns.append(
dict(
label=_("Company"),
fieldname="company",
fieldtype="Link",
options="Company",
width="100",
)
)
self.columns.append(
dict(
label=_("Account"),
fieldname="account",
fieldtype="Link",
options="Account",
width="100",
)
)
self.columns.append(
dict(
label=_("Voucher Type"),
fieldname="voucher_type",
fieldtype="Link",
options="DocType",
width="100",
)
)
self.columns.append(
dict(
label=_("Voucher No"),
fieldname="voucher_no",
fieldtype="Data",
options=options,
fieldtype="Dynamic Link",
options="voucher_type",
width="100",
)
)
self.columns.append(
dict(
label=_("Party Type"),
fieldname="party_type",
fieldtype="Link",
options="DocType",
width="100",
)
)
@ -176,8 +232,8 @@ class General_Payment_Ledger_Comparison(object):
dict(
label=_("Party"),
fieldname="party",
fieldtype="Data",
options=options,
fieldtype="Dynamic Link",
options="party_type",
width="100",
)
)

View File

@ -50,7 +50,11 @@ class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
self.assertEqual(len(data), 1)
expected = {
"company": sinv.company,
"account": sinv.debit_to,
"voucher_type": sinv.doctype,
"voucher_no": sinv.name,
"party_type": "Customer",
"party": sinv.customer,
"gl_balance": sinv.grand_total,
"pl_balance": sinv.grand_total - 1,

View File

@ -175,7 +175,7 @@ frappe.query_reports["General Ledger"] = {
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"label": __("Include Default FB Entries"),
"fieldtype": "Check",
"default": 1
},
@ -193,7 +193,13 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "add_values_in_transaction_currency",
"label": __("Add Columns in Transaction Currency"),
"fieldtype": "Check"
},
{
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check"
}
]
}

View File

@ -163,6 +163,9 @@ def get_gl_entries(filters, accounting_dimensions):
select_fields = """, debit, credit, debit_in_account_currency,
credit_in_account_currency """
if filters.get("show_remarks"):
select_fields += """,remarks"""
order_by_statement = "order by posting_date, account, creation"
if filters.get("include_dimensions"):
@ -195,7 +198,7 @@ def get_gl_entries(filters, accounting_dimensions):
voucher_type, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening, creation {select_fields}
against, is_opening, creation {select_fields}
from `tabGL Entry`
where company=%(company)s {conditions}
{order_by_statement}
@ -256,9 +259,7 @@ def get_conditions(filters):
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
filters.get("company_fb")
):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
else:
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
else:
@ -631,8 +632,10 @@ def get_columns(filters):
"width": 100,
},
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
{"label": _("Remarks"), "fieldname": "remarks", "width": 400},
]
)
if filters.get("show_remarks"):
columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}])
return columns

View File

@ -47,6 +47,7 @@ def get_result(
out = []
for name, details in gle_map.items():
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
bill_no, bill_date = "", ""
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
@ -70,7 +71,10 @@ def get_result(
if net_total_map.get(name):
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
total_amount = grand_total = base_total = tax_amount / (rate / 100)
if rate:
total_amount = grand_total = base_total = tax_amount / (rate / 100)
elif voucher_type == "Purchase Invoice":
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name)
else:
total_amount, grand_total, base_total = net_total_map.get(name)
else:
@ -96,7 +100,7 @@ def get_result(
row.update(
{
"section_code": tax_withholding_category,
"section_code": tax_withholding_category or "",
"entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
@ -106,10 +110,14 @@ def get_result(
"transaction_date": posting_date,
"transaction_type": voucher_type,
"ref_no": name,
"supplier_invoice_no": bill_no,
"supplier_invoice_date": bill_date,
}
)
out.append(row)
out.sort(key=lambda x: x["section_code"])
return out
@ -157,14 +165,14 @@ def get_gle_map(documents):
def get_columns(filters):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 90,
},
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
]
if filters.naming_series == "Naming Series":
@ -179,51 +187,60 @@ def get_columns(filters):
columns.extend(
[
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 90,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 90,
},
]
)
if filters.party_type == "Supplier":
columns.extend(
[
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
]
)
columns.extend(
[
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 90,
"width": 60,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"label": _("Total Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 90,
},
{
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Float",
"width": 90,
"width": 120,
},
{
"label": _("Base Total"),
"fieldname": "base_total",
"fieldtype": "Float",
"width": 90,
"width": 120,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Float",
"width": 120,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
{
"label": _("Reference No."),
"fieldname": "ref_no",
@ -231,6 +248,12 @@ def get_columns(filters):
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
]
)
@ -253,27 +276,7 @@ def get_tds_docs(filters):
"Tax Withholding Account", {"company": filters.get("company")}, pluck="account"
)
query_filters = {
"account": ("in", tds_accounts),
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
"is_cancelled": 0,
"against": ("not in", bank_accounts),
}
party = frappe.get_all(filters.get("party_type"), pluck="name")
or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
if filters.get("party"):
del query_filters["account"]
del query_filters["against"]
or_filters = {"against": filters.get("party"), "party": filters.get("party")}
tds_docs = frappe.get_all(
"GL Entry",
filters=query_filters,
or_filters=or_filters,
fields=["voucher_no", "voucher_type", "against", "party"],
)
tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True)
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":
@ -309,6 +312,47 @@ def get_tds_docs(filters):
)
def get_tds_docs_query(filters, bank_accounts, tds_accounts):
if not tds_accounts:
frappe.throw(
_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")),
title=_("Accounts Missing Error"),
)
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select("voucher_no", "voucher_type", "against", "party")
.where((gle.is_cancelled == 0))
)
if filters.get("from_date"):
query = query.where(gle.posting_date >= filters.get("from_date"))
if filters.get("to_date"):
query = query.where(gle.posting_date <= filters.get("to_date"))
if bank_accounts:
query = query.where(gle.against.notin(bank_accounts))
if filters.get("party"):
party = [filters.get("party")]
query = query.where(
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
| gle.party.isin(party)
)
else:
party = frappe.get_all(filters.get("party_type"), pluck="name")
query = query.where(
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
| (
(gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
)
| gle.party.isin(party)
)
return query
def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
@ -335,6 +379,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"base_tax_withholding_net_total",
"grand_total",
"base_total",
"bill_no",
"bill_date",
],
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
"Payment Entry": [
@ -353,7 +399,13 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
value = [
entry.base_tax_withholding_net_total,
entry.grand_total,
entry.base_total,
entry.bill_no,
entry.bill_date,
]
elif doctype == "Sales Invoice":
value = [entry.base_net_total, entry.grand_total, entry.base_total]
elif doctype == "Payment Entry":

View File

@ -95,7 +95,7 @@ frappe.query_reports["Trial Balance"] = {
},
{
"fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"),
"label": __("Include Default FB Entries"),
"fieldtype": "Check",
"default": 1
},

View File

@ -275,9 +275,7 @@ def get_opening_balance(
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
)
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@ -10,7 +10,7 @@ import frappe.defaults
from frappe import _, qb, throw
from frappe.model.meta import get_field_precision
from frappe.query_builder import AliasedQuery, Criterion, Table
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Round, Sum
from frappe.query_builder.utils import DocType
from frappe.utils import (
cint,
@ -536,6 +536,8 @@ def check_if_advance_entry_modified(args):
)
else:
precision = frappe.get_precision("Payment Entry", "unallocated_amount")
payment_entry = frappe.qb.DocType("Payment Entry")
payment_ref = frappe.qb.DocType("Payment Entry Reference")
@ -557,7 +559,10 @@ def check_if_advance_entry_modified(args):
.where(payment_ref.allocated_amount == args.get("unreconciled_amount"))
)
else:
q = q.where(payment_entry.unallocated_amount == args.get("unreconciled_amount"))
q = q.where(
Round(payment_entry.unallocated_amount, precision)
== Round(args.get("unreconciled_amount"), precision)
)
ret = q.run(as_dict=True)
@ -2041,3 +2046,7 @@ def create_gain_loss_journal(
journal_entry.save()
journal_entry.submit()
return journal_entry.name
def get_party_types_from_account_type(account_type):
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")

View File

@ -818,7 +818,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
"depreciation_method": d.depreciation_method,
"total_number_of_depreciations": d.total_number_of_depreciations,
"frequency_of_depreciation": d.frequency_of_depreciation,
"daily_depreciation": d.daily_depreciation,
"daily_prorata_based": d.daily_prorata_based,
"salvage_value_percentage": d.salvage_value_percentage,
"expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100),

View File

@ -780,6 +780,15 @@ def get_disposal_account_and_cost_center(company):
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
asset_doc = frappe.get_doc("Asset", asset)
if asset_doc.available_for_use_date > getdate(disposal_date):
frappe.throw(
"Disposal date {0} cannot be before available for use date {1} of the asset.".format(
disposal_date, asset_doc.available_for_use_date
)
)
elif asset_doc.available_for_use_date == getdate(disposal_date):
return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation)
if not asset_doc.calculate_depreciation:
return flt(asset_doc.value_after_depreciation)

View File

@ -755,7 +755,9 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(schedules, expected_schedules)
def test_schedule_for_straight_line_method_with_daily_depreciation(self):
def test_schedule_for_straight_line_method_with_daily_prorata_based(
self,
):
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2023-01-01",
@ -764,7 +766,7 @@ class TestDepreciationMethods(AssetSetup):
depreciation_start_date="2023-01-31",
total_number_of_depreciations=12,
frequency_of_depreciation=1,
daily_depreciation=1,
daily_prorata_based=1,
)
expected_schedules = [
@ -1760,7 +1762,7 @@ def create_asset(**args):
"total_number_of_depreciations": args.total_number_of_depreciations or 5,
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
"depreciation_start_date": args.depreciation_start_date,
"daily_depreciation": args.daily_depreciation or 0,
"daily_prorata_based": args.daily_prorata_based or 0,
},
)

View File

@ -19,7 +19,7 @@
"depreciation_method",
"total_number_of_depreciations",
"rate_of_depreciation",
"daily_depreciation",
"daily_prorata_based",
"column_break_8",
"frequency_of_depreciation",
"expected_value_after_useful_life",
@ -179,9 +179,9 @@
{
"default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_depreciation",
"fieldname": "daily_prorata_based",
"fieldtype": "Check",
"label": "Daily Depreciation",
"label": "Depreciate based on daily pro-rata",
"print_hide": 1,
"read_only": 1
}
@ -189,7 +189,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-10 22:22:09.722968",
"modified": "2023-11-03 21:32:15.021796",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",

View File

@ -153,7 +153,7 @@ class AssetDepreciationSchedule(Document):
self.frequency_of_depreciation = row.frequency_of_depreciation
self.rate_of_depreciation = row.rate_of_depreciation
self.expected_value_after_useful_life = row.expected_value_after_useful_life
self.daily_depreciation = row.daily_depreciation
self.daily_prorata_based = row.daily_prorata_based
self.status = "Draft"
def make_depr_schedule(
@ -573,7 +573,7 @@ def get_straight_line_or_manual_depr_amount(
)
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
if row.daily_depreciation:
if row.daily_prorata_based:
daily_depr_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / date_diff(
@ -618,7 +618,7 @@ def get_straight_line_or_manual_depr_amount(
) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_depreciation:
if row.daily_prorata_based:
daily_depr_amount = (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)

View File

@ -8,7 +8,7 @@
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
"daily_depreciation",
"daily_prorata_based",
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
@ -86,23 +86,23 @@
"fieldtype": "Percent",
"label": "Rate of Depreciation"
},
{
"default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_depreciation",
"fieldtype": "Check",
"label": "Daily Depreciation"
},
{
"fieldname": "salvage_value_percentage",
"fieldtype": "Percent",
"label": "Salvage Value Percentage"
},
{
"default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_prorata_based",
"fieldtype": "Check",
"label": "Depreciate based on daily pro-rata"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-09-29 15:39:52.740594",
"modified": "2023-11-03 21:30:24.266601",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@ -52,7 +52,7 @@ frappe.query_reports["Fixed Asset Register"] = {
},
{
"fieldname": "include_default_book_assets",
"label": __("Include Default Book Assets"),
"label": __("Include Default FB Assets"),
"fieldtype": "Check",
"default": 1
},

View File

@ -223,7 +223,7 @@ def get_assets_linked_to_fb(filters):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'"))
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Assets'"))
query = query.where(
(afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))

View File

@ -470,6 +470,7 @@
"fieldname": "material_request",
"fieldtype": "Link",
"label": "Material Request",
"mandatory_depends_on": "eval: doc.material_request_item",
"no_copy": 1,
"oldfieldname": "prevdoc_docname",
"oldfieldtype": "Link",
@ -485,6 +486,7 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Material Request Item",
"mandatory_depends_on": "eval: doc.material_request",
"no_copy": 1,
"oldfieldname": "prevdoc_detail_docname",
"oldfieldtype": "Data",
@ -914,7 +916,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-27 15:50:42.655573",
"modified": "2023-11-06 11:00:53.596417",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@ -174,7 +174,7 @@
"fieldname": "supplier_type",
"fieldtype": "Select",
"label": "Supplier Type",
"options": "Company\nIndividual",
"options": "Company\nIndividual\nProprietorship\nPartnership",
"reqd": 1
},
{
@ -485,7 +485,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-09-25 12:48:21.869563",
"modified": "2023-10-19 16:55:15.148325",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@ -2269,6 +2269,7 @@ class AccountsController(TransactionBase):
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
repost_ledger.company = self.company
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
repost_ledger.flags.ignore_permissions = True
repost_ledger.insert()
repost_ledger.submit()
self.db_set("repost_required", 0)

View File

@ -4,7 +4,7 @@
import frappe
from frappe import ValidationError, _, msgprint
from frappe.contacts.doctype.address.address import get_address_display
from frappe.contacts.doctype.address.address import render_address
from frappe.utils import cint, flt, getdate
from frappe.utils.data import nowtime
@ -105,26 +105,26 @@ class BuyingController(SubcontractingController):
def set_rate_for_standalone_debit_note(self):
if self.get("is_return") and self.get("update_stock") and not self.return_against:
for row in self.items:
if row.rate <= 0:
# override the rate with valuation rate
row.rate = get_incoming_rate(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"),
"qty": row.qty,
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
},
raise_error_if_no_rate=False,
)
# override the rate with valuation rate
row.rate = get_incoming_rate(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"),
"qty": row.qty,
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
},
raise_error_if_no_rate=False,
)
row.discount_percentage = 0.0
row.discount_amount = 0.0
row.margin_rate_or_amount = 0.0
row.discount_percentage = 0.0
row.discount_amount = 0.0
row.margin_rate_or_amount = 0.0
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@ -246,7 +246,9 @@ class BuyingController(SubcontractingController):
for address_field, address_display_field in address_dict.items():
if self.get(address_field):
self.set(address_display_field, get_address_display(self.get(address_field)))
self.set(
address_display_field, render_address(self.get(address_field), check_permissions=False)
)
def set_total_in_words(self):
from frappe.utils import money_in_words

View File

@ -611,6 +611,8 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
if filters.get("company"):
condition += "and tabAccount.company = %(company)s"
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
return frappe.db.sql(
"""select tabAccount.name from `tabAccount`
where (tabAccount.report_type = "Profit and Loss"

View File

@ -47,15 +47,15 @@ status_map = {
],
[
"To Bill",
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1",
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed < 100 and self.docstatus == 1",
],
[
"To Deliver",
"eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note",
"eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note",
],
[
"Completed",
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1",
],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],

View File

@ -1210,8 +1210,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
repost_entry = frappe.new_doc("Repost Item Valuation")
repost_entry.based_on = "Item and Warehouse"
repost_entry.voucher_type = voucher_type
repost_entry.voucher_no = voucher_no
repost_entry.item_code = sle.item_code
repost_entry.warehouse = sle.warehouse

View File

@ -192,26 +192,6 @@
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "GoCardless Settings",
"link_count": 0,
"link_to": "GoCardless Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Mpesa Settings",
"link_count": 0,
"link_to": "Mpesa Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -223,7 +203,7 @@
"type": "Link"
}
],
"modified": "2023-08-29 15:48:59.010704",
"modified": "2023-10-31 19:57:32.748726",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",

View File

@ -36,6 +36,7 @@
"prod_plan_references",
"section_break_24",
"combine_sub_items",
"sub_assembly_warehouse",
"section_break_ucc4",
"skip_available_sub_assembly_item",
"column_break_igxl",
@ -416,13 +417,19 @@
{
"fieldname": "column_break_igxl",
"fieldtype": "Column Break"
},
{
"fieldname": "sub_assembly_warehouse",
"fieldtype": "Link",
"label": "Sub Assembly Warehouse",
"options": "Warehouse"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-09-29 11:41:03.246059",
"modified": "2023-11-03 14:08:11.928027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@ -490,6 +490,12 @@ class ProductionPlan(Document):
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
for d in self.sub_assembly_items:
if d.fg_warehouse and d.type_of_manufacturing == "In House":
bin_name = get_or_make_bin(d.production_item, d.fg_warehouse)
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_for_sub_assembly()
def delete_draft_work_order(self):
for d in frappe.get_all(
"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
@ -809,7 +815,11 @@ class ProductionPlan(Document):
bom_data = []
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
warehouse = (
(self.sub_assembly_warehouse or row.warehouse)
if self.skip_available_sub_assembly_item
else None
)
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
sub_assembly_items_store.extend(bom_data)
@ -831,7 +841,7 @@ class ProductionPlan(Document):
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
data.fg_warehouse = row.warehouse
data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or (
"Subcontract" if data.is_sub_contracted_item else "In House"
@ -1637,8 +1647,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
query = query.run()
if not query:
return 0.0
if not query or query[0][0] is None:
return None
reserved_qty_for_production_plan = flt(query[0][0])
@ -1780,3 +1790,29 @@ def sales_order_query(
query = query.offset(start)
return query.run()
def get_reserved_qty_for_sub_assembly(item_code, warehouse):
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Production Plan Sub Assembly Item")
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
.where(
(table.docstatus == 1)
& (child.production_item == item_code)
& (child.fg_warehouse == warehouse)
& (table.status.notin(["Completed", "Closed"]))
)
)
query = query.run()
if not query or query[0][0] is None:
return None
qty = flt(query[0][0])
return qty if qty > 0 else 0.0

View File

@ -1042,13 +1042,14 @@ class TestProductionPlan(FrappeTestCase):
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 1)
pln = frappe.get_doc("Production Plan", pln.name)
pln.cancel()
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln.reload()
self.assertEqual(pln.docstatus, 2)
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_work_order(self):
@ -1359,6 +1360,93 @@ class TestProductionPlan(FrappeTestCase):
if row.item_code == "ChildPart2 For SUB Test":
self.assertEqual(row.quantity, 2)
def test_reserve_sub_assembly_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
bom_tree = {
"Fininshed Goods Bicycle": {
"Frame Assembly": {"Frame": {}},
"Chain Assembly": {"Chain": {}},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
warehouse = "_Test Warehouse - _TC"
company = "_Test Company"
sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company)
for item_code in ["Frame", "Chain"]:
make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100)
before_qty = flt(
frappe.db.get_value(
"Bin",
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
"reserved_qty_for_production_plan",
)
)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=2,
ignore_existing_ordered_qty=1,
do_not_submit=1,
skip_available_sub_assembly_item=1,
warehouse=warehouse,
sub_assembly_warehouse=sub_assembly_warehouse,
)
plan.get_sub_assembly_items()
plan.submit()
after_qty = flt(
frappe.db.get_value(
"Bin",
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
"reserved_qty_for_production_plan",
)
)
self.assertEqual(after_qty, before_qty + 2)
plan.make_work_order()
work_orders = frappe.get_all(
"Work Order",
fields=["name", "production_item"],
filters={"production_plan": plan.name},
order_by="creation desc",
)
for d in work_orders:
wo_doc = frappe.get_doc("Work Order", d.name)
wo_doc.skip_transfer = 1
wo_doc.from_wip_warehouse = 1
wo_doc.wip_warehouse = (
warehouse
if d.production_item in ["Frame Assembly", "Chain Assembly"]
else sub_assembly_warehouse
)
wo_doc.submit()
if d.production_item == "Frame Assembly":
self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse)
se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2))
se_doc.submit()
after_qty = flt(
frappe.db.get_value(
"Bin",
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
"reserved_qty_for_production_plan",
)
)
self.assertEqual(after_qty, before_qty)
def create_production_plan(**args):
"""
@ -1379,6 +1467,7 @@ def create_production_plan(**args):
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
"get_items_from": "Sales Order",
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
"sub_assembly_warehouse": args.sub_assembly_warehouse,
}
)

View File

@ -17,11 +17,10 @@
"type_of_manufacturing",
"supplier",
"work_order_details_section",
"work_order",
"wo_produced_qty",
"purchase_order",
"production_plan_item",
"column_break_7",
"produced_qty",
"received_qty",
"indent",
"section_break_19",
@ -52,13 +51,6 @@
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@ -81,7 +73,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty"
"label": "Received Qty",
"read_only": 1
},
{
"fieldname": "bom_no",
@ -161,12 +154,6 @@
"label": "Target Warehouse",
"options": "Warehouse"
},
{
"fieldname": "produced_qty",
"fieldtype": "Data",
"label": "Produced Quantity",
"read_only": 1
},
{
"default": "In House",
"fieldname": "type_of_manufacturing",
@ -209,12 +196,18 @@
"label": "Projected Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "wo_produced_qty",
"fieldtype": "Float",
"label": "Produced Qty",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-05-22 17:52:34.708879",
"modified": "2023-11-03 13:33:42.959387",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",

View File

@ -710,7 +710,7 @@ erpnext.work_order = {
return new Promise((resolve, reject) => {
frappe.prompt({
fieldtype: 'Float',
label: __('Qty for {0}', [purpose]),
label: __('Qty for {0}', [__(purpose)]),
fieldname: 'qty',
description: __('Max: {0}', [max]),
default: max

View File

@ -293,6 +293,7 @@ class WorkOrder(Document):
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
if self.production_plan:
self.set_produced_qty_for_sub_assembly_item()
self.update_production_plan_status()
def get_transferred_or_manufactured_qty(self, purpose):
@ -569,16 +570,49 @@ class WorkOrder(Document):
)
def update_planned_qty(self):
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_sub_assembly,
)
qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}
if self.production_plan_sub_assembly_item and self.production_plan:
qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly(
self.production_item, self.fg_warehouse
)
update_bin_qty(
self.production_item,
self.fg_warehouse,
{"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)},
qty_dict,
)
if self.material_request:
mr_obj = frappe.get_doc("Material Request", self.material_request)
mr_obj.update_requested_qty([self.material_request_item])
def set_produced_qty_for_sub_assembly_item(self):
table = frappe.qb.DocType("Work Order")
query = (
frappe.qb.from_(table)
.select(Sum(table.produced_qty))
.where(
(table.production_plan == self.production_plan)
& (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item)
& (table.docstatus == 1)
)
).run()
produced_qty = flt(query[0][0]) if query else 0
frappe.db.set_value(
"Production Plan Sub Assembly Item",
self.production_plan_sub_assembly_item,
"wo_produced_qty",
produced_qty,
)
def update_ordered_qty(self):
if (
self.production_plan

View File

@ -343,5 +343,11 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
erpnext.patches.v15_0.update_sre_from_voucher_details
erpnext.patches.v14_0.rename_over_order_allowance_field
erpnext.patches.v14_0.migrate_delivery_stop_lock_field
execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
erpnext.patches.v14_0.add_default_for_repost_settings
erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
erpnext.patches.v15_0.set_reserved_stock_in_bin
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@ -0,0 +1,12 @@
import frappe
def execute():
"""
Update Repost Accounting Ledger Settings with default values
"""
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()

View File

@ -0,0 +1,21 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe.model.utils.rename_field import rename_field
def execute():
try:
rename_field(
"Asset Finance Book", "daily_depreciation", "depreciation_amount_based_on_num_days_in_month"
)
rename_field(
"Asset Depreciation Schedule",
"daily_depreciation",
"depreciation_amount_based_on_num_days_in_month",
)
except Exception as e:
if e.args[0] != 1054:
raise

View File

@ -0,0 +1,21 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe.model.utils.rename_field import rename_field
def execute():
try:
rename_field(
"Asset Finance Book", "depreciation_amount_based_on_num_days_in_month", "daily_prorata_based"
)
rename_field(
"Asset Depreciation Schedule",
"depreciation_amount_based_on_num_days_in_month",
"daily_prorata_based",
)
except Exception as e:
if e.args[0] != 1054:
raise

View File

@ -0,0 +1,24 @@
import frappe
from frappe.query_builder.functions import Sum
def execute():
sre = frappe.qb.DocType("Stock Reservation Entry")
query = (
frappe.qb.from_(sre)
.select(
sre.item_code,
sre.warehouse,
Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_stock"),
)
.where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"])))
.groupby(sre.item_code, sre.warehouse)
)
for d in query.run(as_dict=True):
frappe.db.set_value(
"Bin",
{"item_code": d.item_code, "warehouse": d.warehouse},
"reserved_stock",
d.reserved_stock,
)

View File

@ -1,155 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import flt, time_diff_in_hours
def get_columns():
return [
{
"label": _("Employee ID"),
"fieldtype": "Link",
"fieldname": "employee",
"options": "Employee",
"width": 300,
},
{
"label": _("Employee Name"),
"fieldtype": "data",
"fieldname": "employee_name",
"hidden": 1,
"width": 200,
},
{
"label": _("Timesheet"),
"fieldtype": "Link",
"fieldname": "timesheet",
"options": "Timesheet",
"width": 150,
},
{"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", "width": 150},
{
"label": _("Billable Hours"),
"fieldtype": "Float",
"fieldname": "total_billable_hours",
"width": 150,
},
{"label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", "width": 150},
]
def get_data(filters):
data = []
if filters.from_date > filters.to_date:
frappe.msgprint(_("From Date can not be greater than To Date"))
return data
timesheets = get_timesheets(filters)
filters.from_date = frappe.utils.get_datetime(filters.from_date)
filters.to_date = frappe.utils.add_to_date(
frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1
)
timesheet_details = get_timesheet_details(filters, timesheets.keys())
for ts, ts_details in timesheet_details.items():
total_hours = 0
total_billing_hours = 0
total_amount = 0
for row in ts_details:
from_time, to_time = filters.from_date, filters.to_date
if row.to_time < from_time or row.from_time > to_time:
continue
if row.from_time > from_time:
from_time = row.from_time
if row.to_time < to_time:
to_time = row.to_time
activity_duration, billing_duration = get_billable_and_total_duration(row, from_time, to_time)
total_hours += activity_duration
total_billing_hours += billing_duration
total_amount += billing_duration * flt(row.billing_rate)
if total_hours:
data.append(
{
"employee": timesheets.get(ts).employee,
"employee_name": timesheets.get(ts).employee_name,
"timesheet": ts,
"total_billable_hours": total_billing_hours,
"total_hours": total_hours,
"amount": total_amount,
}
)
return data
def get_timesheets(filters):
record_filters = [
["start_date", "<=", filters.to_date],
["end_date", ">=", filters.from_date],
]
if not filters.get("include_draft_timesheets"):
record_filters.append(["docstatus", "=", 1])
else:
record_filters.append(["docstatus", "!=", 2])
if "employee" in filters:
record_filters.append(["employee", "=", filters.employee])
timesheets = frappe.get_all(
"Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"]
)
timesheet_map = frappe._dict()
for d in timesheets:
timesheet_map.setdefault(d.name, d)
return timesheet_map
def get_timesheet_details(filters, timesheet_list):
timesheet_details_filter = {"parent": ["in", timesheet_list]}
if "project" in filters:
timesheet_details_filter["project"] = filters.project
timesheet_details = frappe.get_all(
"Timesheet Detail",
filters=timesheet_details_filter,
fields=[
"from_time",
"to_time",
"hours",
"is_billable",
"billing_hours",
"billing_rate",
"parent",
],
)
timesheet_details_map = frappe._dict()
for d in timesheet_details:
timesheet_details_map.setdefault(d.parent, []).append(d)
return timesheet_details_map
def get_billable_and_total_duration(activity, start_time, end_time):
precision = frappe.get_precision("Timesheet Detail", "hours")
activity_duration = time_diff_in_hours(end_time, start_time)
billing_duration = 0.0
if activity.is_billable:
billing_duration = activity.billing_hours
if activity_duration != activity.billing_hours:
billing_duration = activity_duration * activity.billing_hours / activity.hours
return flt(activity_duration, precision), flt(billing_duration, precision)

View File

@ -1,34 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Employee Billing Summary"] = {
"filters": [
{
fieldname: "employee",
label: __("Employee"),
fieldtype: "Link",
options: "Employee",
reqd: 1
},
{
fieldname:"from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
reqd: 1
},
{
fieldname:"include_draft_timesheets",
label: __("Include Timesheets in Draft Status"),
fieldtype: "Check",
},
]
}

View File

@ -1,36 +0,0 @@
{
"add_total_row": 1,
"creation": "2019-03-08 15:08:19.929728",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2019-06-13 15:54:49.213973",
"modified_by": "Administrator",
"module": "Projects",
"name": "Employee Billing Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
"report_name": "Employee Billing Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Projects User"
},
{
"role": "HR User"
},
{
"role": "Manufacturing User"
},
{
"role": "Employee"
},
{
"role": "Accounts User"
}
]
}

View File

@ -1,15 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from erpnext.projects.report.billing_summary import get_columns, get_data
def execute(filters=None):
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data

View File

@ -1,34 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Project Billing Summary"] = {
"filters": [
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project",
reqd: 1
},
{
fieldname:"from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.month_start(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
reqd: 1
},
{
fieldname:"include_draft_timesheets",
label: __("Include Timesheets in Draft Status"),
fieldtype: "Check",
},
]
}

View File

@ -1,15 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from erpnext.projects.report.billing_summary import get_columns, get_data
def execute(filters=None):
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data

View File

@ -0,0 +1,67 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Timesheet Billing Summary"] = {
tree: true,
initial_depth: 0,
filters: [
{
fieldname: "employee",
label: __("Employee"),
fieldtype: "Link",
options: "Employee",
on_change: function (report) {
unset_group_by(report, "employee");
},
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project",
on_change: function (report) {
unset_group_by(report, "project");
},
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(
frappe.datetime.month_start(),
-1
),
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.add_days(
frappe.datetime.month_start(),
-1
),
},
{ // NOTE: `update_group_by_options` expects this filter to be the fifth in the list
fieldname: "group_by",
label: __("Group By"),
fieldtype: "Select",
options: [
"",
{ value: "employee", label: __("Employee") },
{ value: "project", label: __("Project") },
{ value: "date", label: __("Start Date") },
],
},
{
fieldname: "include_draft_timesheets",
label: __("Include Timesheets in Draft Status"),
fieldtype: "Check",
},
],
};
function unset_group_by(report, fieldname) {
if (report.get_filter_value(fieldname) && report.get_filter_value("group_by") == fieldname) {
report.set_filter_value("group_by", "");
}
}

View File

@ -1,36 +1,42 @@
{
"add_total_row": 1,
"creation": "2019-03-11 16:22:39.460524",
"disable_prepared_report": 0,
"columns": [],
"creation": "2023-10-10 23:53:43.692067",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2019-06-13 15:54:55.255947",
"letter_head": "ALYF GmbH",
"letterhead": null,
"modified": "2023-10-11 00:58:30.639078",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Billing Summary",
"name": "Timesheet Billing Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
"report_name": "Project Billing Summary",
"report_name": "Timesheet Billing Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Projects User"
},
{
"role": "HR User"
"role": "Employee"
},
{
"role": "Accounts User"
},
{
"role": "Manufacturing User"
},
{
"role": "Employee"
"role": "HR User"
},
{
"role": "Accounts User"
"role": "Employee Self Service"
}
]
}

View File

@ -0,0 +1,146 @@
import frappe
from frappe import _
from frappe.model.docstatus import DocStatus
def execute(filters=None):
group_fieldname = filters.pop("group_by", None)
filters = frappe._dict(filters or {})
columns = get_columns(filters, group_fieldname)
data = get_data(filters, group_fieldname)
return columns, data
def get_columns(filters, group_fieldname=None):
group_columns = {
"date": {
"label": _("Date"),
"fieldtype": "Date",
"fieldname": "date",
"width": 150,
},
"project": {
"label": _("Project"),
"fieldtype": "Link",
"fieldname": "project",
"options": "Project",
"width": 200,
"hidden": int(bool(filters.get("project"))),
},
"employee": {
"label": _("Employee ID"),
"fieldtype": "Link",
"fieldname": "employee",
"options": "Employee",
"width": 200,
"hidden": int(bool(filters.get("employee"))),
},
}
columns = []
if group_fieldname:
columns.append(group_columns.get(group_fieldname))
columns.extend(
column for column in group_columns.values() if column.get("fieldname") != group_fieldname
)
else:
columns.extend(group_columns.values())
columns.extend(
[
{
"label": _("Employee Name"),
"fieldtype": "data",
"fieldname": "employee_name",
"hidden": 1,
},
{
"label": _("Timesheet"),
"fieldtype": "Link",
"fieldname": "timesheet",
"options": "Timesheet",
"width": 150,
},
{"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "hours", "width": 150},
{
"label": _("Billing Hours"),
"fieldtype": "Float",
"fieldname": "billing_hours",
"width": 150,
},
{
"label": _("Billing Amount"),
"fieldtype": "Currency",
"fieldname": "billing_amount",
"width": 150,
},
]
)
return columns
def get_data(filters, group_fieldname=None):
_filters = []
if filters.get("employee"):
_filters.append(("employee", "=", filters.get("employee")))
if filters.get("project"):
_filters.append(("Timesheet Detail", "project", "=", filters.get("project")))
if filters.get("from_date"):
_filters.append(("Timesheet Detail", "from_time", ">=", filters.get("from_date")))
if filters.get("to_date"):
_filters.append(("Timesheet Detail", "to_time", "<=", filters.get("to_date")))
if not filters.get("include_draft_timesheets"):
_filters.append(("docstatus", "=", DocStatus.submitted()))
else:
_filters.append(("docstatus", "in", (DocStatus.submitted(), DocStatus.draft())))
data = frappe.get_list(
"Timesheet",
fields=[
"name as timesheet",
"`tabTimesheet`.employee",
"`tabTimesheet`.employee_name",
"`tabTimesheet Detail`.from_time as date",
"`tabTimesheet Detail`.project",
"`tabTimesheet Detail`.hours",
"`tabTimesheet Detail`.billing_hours",
"`tabTimesheet Detail`.billing_amount",
],
filters=_filters,
order_by="`tabTimesheet Detail`.from_time",
)
return group_by(data, group_fieldname) if group_fieldname else data
def group_by(data, fieldname):
groups = {row.get(fieldname) for row in data}
grouped_data = []
for group in sorted(groups):
group_row = {
fieldname: group,
"hours": sum(row.get("hours") for row in data if row.get(fieldname) == group),
"billing_hours": sum(row.get("billing_hours") for row in data if row.get(fieldname) == group),
"billing_amount": sum(row.get("billing_amount") for row in data if row.get(fieldname) == group),
"indent": 0,
"is_group": 1,
}
if fieldname == "employee":
group_row["employee_name"] = next(
row.get("employee_name") for row in data if row.get(fieldname) == group
)
grouped_data.append(group_row)
for row in data:
if row.get(fieldname) != group:
continue
_row = row.copy()
_row[fieldname] = None
_row["indent"] = 1
_row["is_group"] = 0
grouped_data.append(_row)
return grouped_data

View File

@ -155,9 +155,9 @@
"dependencies": "Project",
"hidden": 0,
"is_query_report": 1,
"label": "Project Billing Summary",
"label": "Timesheet Billing Summary",
"link_count": 0,
"link_to": "Project Billing Summary",
"link_to": "Timesheet Billing Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
@ -192,7 +192,7 @@
"type": "Link"
}
],
"modified": "2023-07-04 14:39:08.935853",
"modified": "2023-10-10 23:54:33.082108",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",
@ -234,8 +234,8 @@
"type": "DocType"
},
{
"label": "Project Billing Summary",
"link_to": "Project Billing Summary",
"label": "Timesheet Billing Summary",
"link_to": "Timesheet Billing Summary",
"type": "Report"
},
{

View File

@ -3,10 +3,10 @@
frappe.ui.form.on('Quality Procedure', {
refresh: function(frm) {
frm.set_query("procedure","processes", (frm) =>{
frm.set_query('procedure', 'processes', (frm) =>{
return {
filters: {
name: ["not in", [frm.parent_quality_procedure, frm.name]]
name: ['not in', [frm.parent_quality_procedure, frm.name]]
}
};
});
@ -14,7 +14,8 @@ frappe.ui.form.on('Quality Procedure', {
frm.set_query('parent_quality_procedure', function(){
return {
filters: {
is_group: 1
is_group: 1,
name: ['!=', frm.doc.name]
}
};
});

View File

@ -16,16 +16,13 @@ class QualityProcedure(NestedSet):
def on_update(self):
NestedSet.on_update(self)
self.set_parent()
self.remove_parent_from_old_child()
self.add_child_to_parent()
self.remove_child_from_old_parent()
def after_insert(self):
self.set_parent()
# add child to parent if missing
if self.parent_quality_procedure:
parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
if not [d for d in parent.processes if d.procedure == self.name]:
parent.append("processes", {"procedure": self.name, "process_description": self.name})
parent.save()
self.add_child_to_parent()
def on_trash(self):
# clear from child table (sub procedures)
@ -36,15 +33,6 @@ class QualityProcedure(NestedSet):
)
NestedSet.on_trash(self, allow_root_deletion=True)
def set_parent(self):
for process in self.processes:
# Set parent for only those children who don't have a parent
has_parent = frappe.db.get_value(
"Quality Procedure", process.procedure, "parent_quality_procedure"
)
if not has_parent and process.procedure:
frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name)
def check_for_incorrect_child(self):
for process in self.processes:
if process.procedure:
@ -61,6 +49,48 @@ class QualityProcedure(NestedSet):
title=_("Invalid Child Procedure"),
)
def set_parent(self):
"""Set `Parent Procedure` in `Child Procedures`"""
for process in self.processes:
if process.procedure:
if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"):
frappe.db.set_value(
"Quality Procedure", process.procedure, "parent_quality_procedure", self.name
)
def remove_parent_from_old_child(self):
"""Remove `Parent Procedure` from `Old Child Procedures`"""
if old_doc := self.get_doc_before_save():
if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]):
current_child_procedures = set([d.procedure for d in self.processes if d.procedure])
if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)):
for child_procedure in removed_child_procedures:
frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None)
def add_child_to_parent(self):
"""Add `Child Procedure` to `Parent Procedure`"""
if self.parent_quality_procedure:
parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
if not [d for d in parent.processes if d.procedure == self.name]:
parent.append("processes", {"procedure": self.name, "process_description": self.name})
parent.save()
def remove_child_from_old_parent(self):
"""Remove `Child Procedure` from `Old Parent Procedure`"""
if old_doc := self.get_doc_before_save():
if old_parent := old_doc.parent_quality_procedure:
if self.parent_quality_procedure != old_parent:
parent = frappe.get_doc("Quality Procedure", old_parent)
for process in parent.processes:
if process.procedure == self.name:
parent.remove(process)
parent.save()
@frappe.whitelist()
def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False):

View File

@ -1,56 +1,107 @@
# Copyright (c) 2018, Frappe and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from .quality_procedure import add_node
class TestQualityProcedure(unittest.TestCase):
class TestQualityProcedure(FrappeTestCase):
def test_add_node(self):
try:
procedure = frappe.get_doc(
dict(
doctype="Quality Procedure",
quality_procedure_name="Test Procedure 1",
processes=[dict(process_description="Test Step 1")],
)
).insert()
frappe.local.form_dict = frappe._dict(
doctype="Quality Procedure",
quality_procedure_name="Test Child 1",
parent_quality_procedure=procedure.name,
cmd="test",
is_root="false",
)
node = add_node()
procedure.reload()
self.assertEqual(procedure.is_group, 1)
# child row created
self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
node.delete()
procedure.reload()
# child unset
self.assertFalse([d for d in procedure.processes if d.name == node.name])
finally:
procedure.delete()
def create_procedure():
return frappe.get_doc(
dict(
doctype="Quality Procedure",
quality_procedure_name="Test Procedure 1",
is_group=1,
processes=[dict(process_description="Test Step 1")],
procedure = create_procedure(
{
"quality_procedure_name": "Test Procedure 1",
"is_group": 1,
"processes": [dict(process_description="Test Step 1")],
}
)
).insert()
frappe.local.form_dict = frappe._dict(
doctype="Quality Procedure",
quality_procedure_name="Test Child 1",
parent_quality_procedure=procedure.name,
cmd="test",
is_root="false",
)
node = add_node()
procedure.reload()
self.assertEqual(procedure.is_group, 1)
# child row created
self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
node.delete()
procedure.reload()
# child unset
self.assertFalse([d for d in procedure.processes if d.name == node.name])
def test_remove_parent_from_old_child(self):
child_qp = create_procedure(
{
"quality_procedure_name": "Test Child 1",
"is_group": 0,
}
)
group_qp = create_procedure(
{
"quality_procedure_name": "Test Group",
"is_group": 1,
"processes": [dict(procedure=child_qp.name)],
}
)
child_qp.reload()
self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
group_qp.reload()
del group_qp.processes[0]
group_qp.save()
child_qp.reload()
self.assertEqual(child_qp.parent_quality_procedure, None)
def remove_child_from_old_parent(self):
child_qp = create_procedure(
{
"quality_procedure_name": "Test Child 1",
"is_group": 0,
}
)
group_qp = create_procedure(
{
"quality_procedure_name": "Test Group",
"is_group": 1,
"processes": [dict(procedure=child_qp.name)],
}
)
group_qp.reload()
self.assertTrue([d for d in group_qp.processes if d.procedure == child_qp.name])
child_qp.reload()
self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
child_qp.parent_quality_procedure = None
child_qp.save()
group_qp.reload()
self.assertFalse([d for d in group_qp.processes if d.procedure == child_qp.name])
def create_procedure(kwargs=None):
kwargs = frappe._dict(kwargs or {})
doc = frappe.new_doc("Quality Procedure")
doc.quality_procedure_name = kwargs.quality_procedure_name or "_Test Procedure"
doc.is_group = kwargs.is_group or 0
for process in kwargs.processes or []:
doc.append("processes", process)
doc.insert()
return doc

View File

@ -134,7 +134,7 @@
"label": "Customer Type",
"oldfieldname": "customer_type",
"oldfieldtype": "Select",
"options": "Company\nIndividual",
"options": "Company\nIndividual\nProprietorship\nPartnership",
"reqd": 1
},
{
@ -584,7 +584,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-09-21 12:23:20.706020",
"modified": "2023-10-19 16:56:27.327035",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@ -217,7 +217,15 @@ class SalesOrder(SellingController):
def validate_with_previous_doc(self):
super(SalesOrder, self).validate_with_previous_doc(
{"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}}
{
"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]},
"Quotation Item": {
"ref_dn_field": "quotation_item",
"compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]],
"is_child_table": True,
"allow_duplicate_prev_row_id": True,
},
}
)
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):

View File

@ -5,20 +5,24 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"warehouse",
"item_code",
"reserved_qty",
"column_break_yreo",
"warehouse",
"section_break_stag",
"actual_qty",
"ordered_qty",
"indented_qty",
"planned_qty",
"indented_qty",
"ordered_qty",
"projected_qty",
"column_break_xn5j",
"reserved_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
"reserved_qty_for_production_plan",
"ma_rate",
"reserved_stock",
"section_break_pmrs",
"stock_uom",
"fcfs_rate",
"column_break_0slj",
"valuation_rate",
"stock_value"
],
@ -56,7 +60,7 @@
"fieldname": "reserved_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Reserved Quantity",
"label": "Reserved Qty",
"oldfieldname": "reserved_qty",
"oldfieldtype": "Currency",
"read_only": 1
@ -67,7 +71,7 @@
"fieldtype": "Float",
"in_filter": 1,
"in_list_view": 1,
"label": "Actual Quantity",
"label": "Actual Qty",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"read_only": 1
@ -77,7 +81,7 @@
"fieldname": "ordered_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Ordered Quantity",
"label": "Ordered Qty",
"oldfieldname": "ordered_qty",
"oldfieldtype": "Currency",
"read_only": 1
@ -86,7 +90,7 @@
"default": "0.00",
"fieldname": "indented_qty",
"fieldtype": "Float",
"label": "Requested Quantity",
"label": "Requested Qty",
"oldfieldname": "indented_qty",
"oldfieldtype": "Currency",
"read_only": 1
@ -116,20 +120,9 @@
{
"fieldname": "reserved_qty_for_sub_contract",
"fieldtype": "Float",
"label": "Reserved Qty for sub contract",
"label": "Reserved Qty for Subcontract",
"read_only": 1
},
{
"fieldname": "ma_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Moving Average Rate",
"oldfieldname": "ma_rate",
"oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
@ -140,17 +133,6 @@
"options": "UOM",
"read_only": 1
},
{
"fieldname": "fcfs_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "FCFS Rate",
"oldfieldname": "fcfs_rate",
"oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
@ -172,13 +154,40 @@
"fieldtype": "Float",
"label": "Reserved Qty for Production Plan",
"read_only": 1
},
{
"fieldname": "section_break_stag",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_yreo",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_xn5j",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_pmrs",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_0slj",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "reserved_stock",
"fieldtype": "Float",
"label": "Reserved Stock",
"read_only": 1
}
],
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2023-05-02 23:26:21.806965",
"modified": "2023-11-01 16:51:17.079107",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",

View File

@ -34,10 +34,15 @@ class Bin(Document):
get_reserved_qty_for_production_plan,
)
self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
self.item_code, self.warehouse
)
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
return
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
@ -48,6 +53,29 @@ class Bin(Document):
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_qty_for_for_sub_assembly(self):
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_reserved_qty_for_sub_assembly,
)
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
self.item_code, self.warehouse
)
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
return
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
self.set_projected_qty()
self.db_set(
{
"projected_qty": self.projected_qty,
"reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan),
},
update_modified=True,
)
def update_reserved_qty_for_production(self):
"""Update qty reserved for production from Production Item tables
in open work orders"""
@ -148,6 +176,17 @@ class Bin(Document):
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
def update_reserved_stock(self):
"""Update `Reserved Stock` on change in Reserved Qty of Stock Reservation Entry"""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse,
)
reserved_stock = get_sre_reserved_qty_for_item_and_warehouse(self.item_code, self.warehouse)
self.db_set("reserved_stock", flt(reserved_stock), update_modified=True)
def on_doctype_update():
frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")

View File

@ -365,6 +365,9 @@ class DeliveryNote(SellingController):
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
# Update Reserved Stock in Bin.
sre_doc.update_reserved_stock_in_bin()
qty_to_deliver -= qty_can_be_deliver
if self._action == "cancel":
@ -427,6 +430,9 @@ class DeliveryNote(SellingController):
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
sre_doc.update_status()
# Update Reserved Stock in Bin.
sre_doc.update_reserved_stock_in_bin()
qty_to_undelivered -= qty_can_be_undelivered
def validate_against_stock_reservation_entries(self):

View File

@ -1029,6 +1029,7 @@ class TestDeliveryNote(FrappeTestCase):
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
si1 = make_sales_invoice(dn1.name)
si1.update_billed_amount_in_delivery_note = True
si1.insert()
si1.submit()
dn1.reload()
@ -1037,6 +1038,7 @@ class TestDeliveryNote(FrappeTestCase):
dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
si2 = make_sales_invoice(dn2.name)
si2.update_billed_amount_in_delivery_note = True
si2.insert()
si2.submit()
dn2.reload()

View File

@ -163,7 +163,7 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item With Item Tax Template",
"tax_category": "_Test Tax Category 2",
"item_tax_template": "",
"item_tax_template": None,
},
{
"item_code": "_Test Item Inherit Group Item Tax Template 1",
@ -178,7 +178,7 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item Inherit Group Item Tax Template 1",
"tax_category": "_Test Tax Category 2",
"item_tax_template": "",
"item_tax_template": None,
},
{
"item_code": "_Test Item Inherit Group Item Tax Template 2",
@ -193,7 +193,7 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item Inherit Group Item Tax Template 2",
"tax_category": "_Test Tax Category 2",
"item_tax_template": "",
"item_tax_template": None,
},
{
"item_code": "_Test Item Override Group Item Tax Template",
@ -208,12 +208,12 @@ class TestItem(FrappeTestCase):
{
"item_code": "_Test Item Override Group Item Tax Template",
"tax_category": "_Test Tax Category 2",
"item_tax_template": "",
"item_tax_template": None,
},
]
expected_item_tax_map = {
"": {},
None: {},
"_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10},
"_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12},
"_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15},

View File

@ -205,7 +205,11 @@ class LandedCostVoucher(Document):
)
docs = frappe.db.get_all(
"Asset",
filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
filters={
receipt_document_type: item.receipt_document,
"item_code": item.item_code,
"docstatus": ["!=", 2],
},
fields=["name", "docstatus"],
)
if not docs or len(docs) != item.qty:

View File

@ -7,7 +7,7 @@ from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import cint, flt, getdate, nowdate
from frappe.utils import cint, flt, get_datetime, getdate, nowdate
from pypika import functions as fn
import erpnext
@ -600,11 +600,10 @@ class PurchaseReceipt(BuyingController):
make_rate_difference_entry(d)
make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount)
elif (
d.warehouse not in warehouse_with_no_account
or d.rejected_warehouse not in warehouse_with_no_account
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account
):
warehouse_with_no_account.append(d.warehouse)
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
if d.is_fixed_asset:
self.update_assets(d, d.valuation_rate)
@ -761,8 +760,12 @@ class PurchaseReceipt(BuyingController):
update_billing_percentage(pr_doc, update_modified=update_modified)
def reserve_stock_for_sales_order(self):
if self.is_return or not cint(
frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase")
if (
self.is_return
or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation")
or not frappe.db.get_single_value(
"Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase"
)
):
return
@ -783,6 +786,11 @@ class PurchaseReceipt(BuyingController):
so_items_details_map.setdefault(item.sales_order, []).append(item_details)
if so_items_details_map:
if get_datetime("{} {}".format(self.posting_date, self.posting_time)) > get_datetime():
return frappe.msgprint(
_("Cannot create Stock Reservation Entries for future dated Purchase Receipts.")
)
for so, items_details in so_items_details_map.items():
so_doc = frappe.get_doc("Sales Order", so)
so_doc.create_stock_reservation_entries(

View File

@ -5,7 +5,7 @@
from unittest.mock import MagicMock, call
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_to_date, now, nowdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@ -196,6 +196,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
riv.set_status("Skipped")
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_prevention_of_cancelled_transaction_riv(self):
frappe.flags.dont_execute_stock_reposts = True
@ -373,6 +374,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
accounts_settings.acc_frozen_upto = ""
accounts_settings.save()
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_create_repost_entry_for_cancelled_document(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",

View File

@ -121,7 +121,7 @@ class SerialandBatchBundle(Document):
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception, title=_("Error"))
def set_incoming_rate(self, row=None, save=False):
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
"Job Card",
@ -131,7 +131,9 @@ class SerialandBatchBundle(Document):
return
if self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(row, save)
self.set_incoming_rate_for_outward_transaction(
row, save, allow_negative_stock=allow_negative_stock
)
else:
self.set_incoming_rate_for_inward_transaction(row, save)
@ -152,7 +154,9 @@ class SerialandBatchBundle(Document):
def get_serial_nos(self):
return [d.serial_no for d in self.entries if d.serial_no]
def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
def set_incoming_rate_for_outward_transaction(
self, row=None, save=False, allow_negative_stock=False
):
sle = self.get_sle_for_outward_transaction()
if self.has_serial_no:
@ -181,7 +185,8 @@ class SerialandBatchBundle(Document):
if self.docstatus == 1:
available_qty += flt(d.qty)
self.validate_negative_batch(d.batch_no, available_qty)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty)
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)

View File

@ -161,7 +161,7 @@ class StockEntry(StockController):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
)
)
self.queue_action("submit", timeout=2000)
@ -172,7 +172,7 @@ class StockEntry(StockController):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
)
)
self.queue_action("cancel", timeout=2000)

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