Merge branch 'develop' of https://github.com/frappe/erpnext into type-error-in-transaction-js
This commit is contained in:
commit
d2057588dd
22
.github/workflows/patch_faux.yml
vendored
Normal file
22
.github/workflows/patch_faux.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
|
||||
|
||||
name: Skipped Patch Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- "**.csv"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Patch Test
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
24
.github/workflows/server-tests-mariadb-faux.yml
vendored
Normal file
24
.github/workflows/server-tests-mariadb-faux.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
|
||||
|
||||
name: Skipped Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
container: [1, 2, 3, 4]
|
||||
|
||||
name: Python Unit Tests
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
@ -187,8 +187,8 @@ class JournalEntry(AccountsController):
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
|
@ -927,8 +927,8 @@ class PaymentEntry(AccountsController):
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
@ -1428,8 +1428,8 @@ class PaymentEntry(AccountsController):
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
|
@ -41,6 +41,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
@ -229,7 +230,7 @@
|
||||
"is_virtual": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-14 13:38:16.264013",
|
||||
"modified": "2024-01-18 11:56:20.234667",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
@ -170,8 +170,8 @@ class PaymentRequest(Document):
|
||||
self.request_phone_payment()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
# set advance payment status
|
||||
ref_doc.set_total_advance_paid()
|
||||
@ -216,8 +216,8 @@ class PaymentRequest(Document):
|
||||
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
# set advance payment status
|
||||
ref_doc.set_total_advance_paid()
|
||||
|
@ -240,7 +240,6 @@ def get_balance_on(
|
||||
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
|
||||
|
||||
if account:
|
||||
|
||||
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
||||
acc.check_permission("read")
|
||||
|
||||
@ -286,18 +285,22 @@ def get_balance_on(
|
||||
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
||||
|
||||
if account or (party_type and party) or account_type:
|
||||
|
||||
precision = get_currency_precision()
|
||||
if in_account_currency:
|
||||
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
|
||||
select_field = (
|
||||
"sum(round(debit_in_account_currency, %s)) - sum(round(credit_in_account_currency, %s))"
|
||||
)
|
||||
else:
|
||||
select_field = "sum(debit) - sum(credit)"
|
||||
select_field = "sum(round(debit, %s)) - sum(round(credit, %s))"
|
||||
|
||||
bal = frappe.db.sql(
|
||||
"""
|
||||
SELECT {0}
|
||||
FROM `tabGL Entry` gle
|
||||
WHERE {1}""".format(
|
||||
select_field, " and ".join(cond)
|
||||
)
|
||||
),
|
||||
(precision, precision),
|
||||
)[0][0]
|
||||
# if bal is None, return 0
|
||||
return flt(bal)
|
||||
@ -619,8 +622,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
if jv_detail.get("reference_type") in advance_payment_doctypes:
|
||||
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
|
||||
|
||||
@ -696,8 +699,8 @@ def update_reference_in_payment_entry(
|
||||
|
||||
# Update Advance Paid in SO/PO since they are getting unlinked
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
if existing_row.get("reference_doctype") in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
existing_row.reference_doctype, existing_row.reference_name
|
||||
|
@ -571,16 +571,16 @@ frappe.ui.form.on('Asset', {
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
|
||||
var asset_quantity = is_grouped_asset ? item.qty : 1;
|
||||
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
|
||||
|
||||
frm.set_value('gross_purchase_amount', purchase_amount);
|
||||
frm.set_value('purchase_receipt_amount', purchase_amount);
|
||||
frm.set_value('asset_quantity', asset_quantity);
|
||||
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
|
||||
if(item.asset_location) { frm.set_value('location', item.asset_location); }
|
||||
frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => {
|
||||
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
|
||||
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
|
||||
|
||||
frm.set_value('gross_purchase_amount', purchase_amount);
|
||||
frm.set_value('purchase_receipt_amount', purchase_amount);
|
||||
frm.set_value('asset_quantity', asset_quantity);
|
||||
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
|
||||
if(item.asset_location) { frm.set_value('location', item.asset_location); }
|
||||
});
|
||||
},
|
||||
|
||||
set_depreciation_rate: function(frm, row) {
|
||||
|
@ -519,14 +519,11 @@ class Asset(AccountsController):
|
||||
movement.cancel()
|
||||
|
||||
def cancel_capitalization(self):
|
||||
asset_capitalization = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"},
|
||||
)
|
||||
|
||||
if asset_capitalization:
|
||||
asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
|
||||
asset_capitalization.cancel()
|
||||
if self.capitalized_in:
|
||||
self.db_set("capitalized_in", None)
|
||||
asset_capitalization = frappe.get_doc("Asset Capitalization", self.capitalized_in)
|
||||
if asset_capitalization.docstatus == 1:
|
||||
asset_capitalization.cancel()
|
||||
|
||||
def delete_depreciation_entries(self):
|
||||
if self.calculate_depreciation:
|
||||
|
@ -561,6 +561,8 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
for row in asset.get("finance_books"):
|
||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
||||
if not asset_depr_schedule_doc:
|
||||
continue
|
||||
|
||||
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
|
||||
if schedule.schedule_date == date:
|
||||
|
@ -146,6 +146,7 @@ class AssetCapitalization(StockController):
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
if asset_doc.docstatus == 1:
|
||||
asset_doc.cancel()
|
||||
|
||||
|
@ -22,6 +22,7 @@ from frappe.utils import (
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
parse_json,
|
||||
today,
|
||||
)
|
||||
|
||||
@ -833,6 +834,37 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges")))
|
||||
|
||||
def append_taxes_from_item_tax_template(self):
|
||||
if not frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
item_tax_rate = row.get("item_tax_rate")
|
||||
if not item_tax_rate:
|
||||
continue
|
||||
|
||||
if isinstance(item_tax_rate, str):
|
||||
item_tax_rate = parse_json(item_tax_rate)
|
||||
|
||||
for account_head, rate in item_tax_rate.items():
|
||||
row = self.get_tax_row(account_head)
|
||||
|
||||
if not row:
|
||||
self.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": account_head,
|
||||
"rate": 0,
|
||||
"description": account_head,
|
||||
},
|
||||
)
|
||||
|
||||
def get_tax_row(self, account_head):
|
||||
for row in self.taxes:
|
||||
if row.account_head == account_head:
|
||||
return row
|
||||
|
||||
def set_other_charges(self):
|
||||
self.set("taxes", [])
|
||||
self.set_taxes()
|
||||
@ -1761,9 +1793,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
|
||||
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
||||
party = self.customer
|
||||
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
|
||||
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
||||
party = self.supplier
|
||||
advance = (
|
||||
frappe.qb.from_(ple)
|
||||
@ -1829,9 +1861,9 @@ class AccountsController(TransactionBase):
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
|
||||
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
||||
new_status = "Requested" if prs else "Not Requested"
|
||||
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
|
||||
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
||||
new_status = "Initiated" if prs else "Not Initiated"
|
||||
|
||||
if new_status == self.advance_payment_status:
|
||||
|
@ -481,8 +481,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
|
||||
|
||||
communication_doctypes = ["Customer", "Supplier"]
|
||||
|
||||
advance_payment_customer_doctypes = ["Sales Order"]
|
||||
advance_payment_supplier_doctypes = ["Purchase Order"]
|
||||
advance_payment_receivable_doctypes = ["Sales Order"]
|
||||
advance_payment_payable_doctypes = ["Purchase Order"]
|
||||
|
||||
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
|
||||
|
||||
|
@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
let warehouse = this.item?.type_of_transaction === "Outward" ?
|
||||
(this.item.warehouse || this.item.s_warehouse) : "";
|
||||
|
||||
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
|
||||
warehouse = this.get_warehouse();
|
||||
}
|
||||
|
||||
return {
|
||||
'item_code': this.item.item_code,
|
||||
'warehouse': ["=", warehouse]
|
||||
|
@ -609,6 +609,61 @@ class TestQuotation(FrappeTestCase):
|
||||
quotation.items[0].conversion_factor = 2.23
|
||||
self.assertRaises(frappe.ValidationError, quotation.save)
|
||||
|
||||
def test_item_tax_template_for_quotation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists("Account", {"account_name": "_Test Vat", "company": "_Test Company"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Vat",
|
||||
"company": "_Test Company",
|
||||
"account_type": "Tax",
|
||||
"root_type": "Asset",
|
||||
"is_group": 0,
|
||||
"parent_account": "Tax Assets - _TC",
|
||||
"tax_rate": 10,
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Item Tax Template", "Vat Template - _TC"):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"name": "Vat Template",
|
||||
"title": "Vat Template",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": "_Test Vat - _TC",
|
||||
"tax_rate": 5,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
item_doc = make_item("_Test Item Tax Template QTN", {"is_stock_item": 1})
|
||||
if not frappe.db.exists(
|
||||
"Item Tax", {"parent": item_doc.name, "item_tax_template": "Vat Template - _TC"}
|
||||
):
|
||||
item_doc.append("taxes", {"item_tax_template": "Vat Template - _TC"})
|
||||
item_doc.save()
|
||||
|
||||
quotation = make_quotation(
|
||||
item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1
|
||||
)
|
||||
self.assertFalse(quotation.taxes)
|
||||
|
||||
quotation.append_taxes_from_item_tax_template()
|
||||
quotation.save()
|
||||
self.assertTrue(quotation.taxes)
|
||||
for row in quotation.taxes:
|
||||
self.assertEqual(row.account_head, "_Test Vat - _TC")
|
||||
self.assertAlmostEqual(row.base_tax_amount, quotation.total * 5 / 100)
|
||||
|
||||
item_doc.taxes = []
|
||||
item_doc.save()
|
||||
|
||||
|
||||
test_records = frappe.get_test_records("Quotation")
|
||||
|
||||
|
@ -118,6 +118,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"print_width": "150px",
|
||||
"reqd": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
@ -908,7 +909,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-24 13:24:55.756320",
|
||||
"modified": "2024-01-25 14:24:00.330219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
@ -43,7 +43,8 @@ class SalesOrderItem(Document):
|
||||
gross_profit: DF.Currency
|
||||
image: DF.Attach | None
|
||||
is_free_item: DF.Check
|
||||
item_code: DF.Link | None
|
||||
is_stock_item: DF.Check
|
||||
item_code: DF.Link
|
||||
item_group: DF.Link | None
|
||||
item_name: DF.Data
|
||||
item_tax_rate: DF.Code | None
|
||||
|
@ -210,7 +210,6 @@ def get_so_with_invoices(filters):
|
||||
.where(
|
||||
(so.docstatus == 1)
|
||||
& (so.status.isin(["To Deliver and Bill", "To Bill", "To Pay"]))
|
||||
& (so.payment_terms_template != "NULL")
|
||||
& (so.company == conditions.company)
|
||||
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
||||
)
|
||||
|
@ -908,8 +908,8 @@ def generate_id_for_deletion_job(company):
|
||||
@frappe.whitelist()
|
||||
def is_deletion_job_running(company):
|
||||
job_id = generate_id_for_deletion_job(company)
|
||||
job_name = get_job(job_id).get_id() # job name will have site prefix
|
||||
if is_job_enqueued(job_id):
|
||||
job_name = get_job(job_id).get_id() # job name will have site prefix
|
||||
frappe.throw(
|
||||
_("A Transaction Deletion Job: {0} is already running for {1}").format(
|
||||
frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company)
|
||||
|
@ -4,9 +4,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestTransactionDeletionRecord(unittest.TestCase):
|
||||
class TestTransactionDeletionRecord(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_company("Dunder Mifflin Paper Co")
|
||||
|
||||
@ -14,7 +15,7 @@ class TestTransactionDeletionRecord(unittest.TestCase):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_doctypes_contain_company_field(self):
|
||||
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
|
||||
tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||
for doctype in tdr.doctypes:
|
||||
contains_company = False
|
||||
doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"]
|
||||
@ -27,17 +28,27 @@ class TestTransactionDeletionRecord(unittest.TestCase):
|
||||
def test_no_of_docs_is_correct(self):
|
||||
for i in range(5):
|
||||
create_task("Dunder Mifflin Paper Co")
|
||||
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
|
||||
tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||
for doctype in tdr.doctypes:
|
||||
if doctype.doctype_name == "Task":
|
||||
self.assertEqual(doctype.no_of_docs, 5)
|
||||
|
||||
def test_deletion_is_successful(self):
|
||||
create_task("Dunder Mifflin Paper Co")
|
||||
create_transaction_deletion_request("Dunder Mifflin Paper Co")
|
||||
create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||
tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"})
|
||||
self.assertEqual(tasks_containing_company, [])
|
||||
|
||||
def test_company_transaction_deletion_request(self):
|
||||
from erpnext.setup.doctype.company.company import create_transaction_deletion_request
|
||||
|
||||
# don't reuse below company for other test cases
|
||||
company = "Deep Space Exploration"
|
||||
create_company(company)
|
||||
|
||||
# below call should not raise any exceptions or throw errors
|
||||
create_transaction_deletion_request(company)
|
||||
|
||||
|
||||
def create_company(company_name):
|
||||
company = frappe.get_doc(
|
||||
@ -46,7 +57,7 @@ def create_company(company_name):
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
|
||||
|
||||
def create_transaction_deletion_request(company):
|
||||
def create_transaction_deletion_doc(company):
|
||||
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
||||
tdr.insert()
|
||||
tdr.submit()
|
||||
|
@ -514,6 +514,13 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten
|
||||
schedule_date() {
|
||||
set_schedule_date(this.frm);
|
||||
}
|
||||
|
||||
qty(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
row.amount = flt(row.qty) * flt(row.rate);
|
||||
frappe.model.set_value(cdt, cdn, "amount", row.amount);
|
||||
refresh_field("amount", row.name, row.parentfield);
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
|
@ -250,6 +250,7 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
for d in self.entries:
|
||||
available_qty = 0
|
||||
|
||||
if self.has_serial_no:
|
||||
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
||||
else:
|
||||
@ -892,6 +893,13 @@ class SerialandBatchBundle(Document):
|
||||
elif batch_nos:
|
||||
self.set("entries", batch_nos)
|
||||
|
||||
def delete_serial_batch_entries(self):
|
||||
SBBE = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
frappe.qb.from_(SBBE).delete().where(SBBE.parent == self.name).run()
|
||||
|
||||
self.set("entries", [])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_blank_csv_template(content):
|
||||
@ -1374,10 +1382,12 @@ def get_available_serial_nos(kwargs):
|
||||
elif kwargs.based_on == "Expiry":
|
||||
order_by = "amc_expiry_date asc"
|
||||
|
||||
filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")}
|
||||
filters = {"item_code": kwargs.item_code}
|
||||
|
||||
if kwargs.warehouse:
|
||||
filters["warehouse"] = kwargs.warehouse
|
||||
if not kwargs.get("ignore_warehouse"):
|
||||
filters["warehouse"] = ("is", "set")
|
||||
if kwargs.warehouse:
|
||||
filters["warehouse"] = kwargs.warehouse
|
||||
|
||||
# Since SLEs are not present against Reserved Stock [POS invoices, SRE], need to ignore reserved serial nos.
|
||||
ignore_serial_nos = get_reserved_serial_nos(kwargs)
|
||||
|
@ -156,6 +156,7 @@ class StockReconciliation(StockController):
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -780,7 +781,20 @@ class StockReconciliation(StockController):
|
||||
|
||||
current_qty = 0.0
|
||||
if row.current_serial_and_batch_bundle:
|
||||
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
|
||||
current_qty = self.get_current_qty_for_serial_or_batch(row)
|
||||
elif row.serial_no:
|
||||
item_dict = get_stock_balance_for(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
voucher_no=self.name,
|
||||
)
|
||||
|
||||
current_qty = item_dict.get("qty")
|
||||
row.current_serial_no = item_dict.get("serial_nos")
|
||||
row.current_valuation_rate = item_dict.get("rate")
|
||||
val_rate = item_dict.get("rate")
|
||||
elif row.batch_no:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||
@ -788,15 +802,16 @@ class StockReconciliation(StockController):
|
||||
|
||||
precesion = row.precision("current_qty")
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
if not row.serial_no:
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
row.current_qty = current_qty
|
||||
@ -842,11 +857,56 @@ class StockReconciliation(StockController):
|
||||
|
||||
return allow_negative_stock
|
||||
|
||||
def get_qty_for_serial_and_batch_bundle(self, row):
|
||||
def get_current_qty_for_serial_or_batch(self, row):
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
||||
precision = doc.entries[0].precision("qty")
|
||||
current_qty = 0.0
|
||||
if doc.has_serial_no:
|
||||
current_qty = self.get_current_qty_for_serial_nos(doc)
|
||||
elif doc.has_batch_no:
|
||||
current_qty = self.get_current_qty_for_batch_nos(doc)
|
||||
|
||||
current_qty = 0
|
||||
return abs(current_qty)
|
||||
|
||||
def get_current_qty_for_serial_nos(self, doc):
|
||||
serial_nos_details = get_available_serial_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": doc.item_code,
|
||||
"warehouse": doc.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"voucher_no": self.name,
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if not serial_nos_details:
|
||||
return 0.0
|
||||
|
||||
doc.delete_serial_batch_entries()
|
||||
current_qty = 0.0
|
||||
for serial_no_row in serial_nos_details:
|
||||
current_qty += 1
|
||||
doc.append(
|
||||
"entries",
|
||||
{
|
||||
"serial_no": serial_no_row.serial_no,
|
||||
"qty": -1,
|
||||
"warehouse": doc.warehouse,
|
||||
"batch_no": serial_no_row.batch_no,
|
||||
},
|
||||
)
|
||||
|
||||
doc.set_incoming_rate(save=True)
|
||||
doc.calculate_qty_and_amount(save=True)
|
||||
doc.db_update_all()
|
||||
|
||||
return current_qty
|
||||
|
||||
def get_current_qty_for_batch_nos(self, doc):
|
||||
current_qty = 0.0
|
||||
precision = doc.entries[0].precision("qty")
|
||||
for d in doc.entries:
|
||||
qty = (
|
||||
get_batch_qty(
|
||||
@ -864,7 +924,7 @@ class StockReconciliation(StockController):
|
||||
|
||||
current_qty += qty
|
||||
|
||||
return abs(current_qty)
|
||||
return current_qty
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
|
@ -925,6 +925,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(len(serial_batch_bundle), 0)
|
||||
|
||||
def test_backdated_purchase_receipt_with_stock_reco(self):
|
||||
item_code = self.make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TEST-SERIAL-.###",
|
||||
}
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Step - 1: Create a Backdated Purchase Receipt
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
||||
)
|
||||
pr1.reload()
|
||||
|
||||
serial_nos = sorted(get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle))[:5]
|
||||
|
||||
# Step - 2: Create a Stock Reconciliation
|
||||
sr1 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=5,
|
||||
serial_no=serial_nos,
|
||||
)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty < 0:
|
||||
self.assertEqual(d.actual_qty, -10.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, -1000.0)
|
||||
else:
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
|
||||
make_purchase_receipt(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
|
||||
)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty < 0:
|
||||
self.assertEqual(d.actual_qty, -20.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, -3000.0)
|
||||
else:
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
active_serial_no = frappe.get_all(
|
||||
"Serial No", filters={"status": "Active", "item_code": item_code}
|
||||
)
|
||||
self.assertEqual(len(active_serial_no), 5)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
@ -99,7 +99,7 @@ frappe.query_reports["Stock Balance"] = {
|
||||
"fieldname": 'ignore_closing_balance',
|
||||
"label": __('Ignore Closing Balance'),
|
||||
"fieldtype": 'Check',
|
||||
"default": 1
|
||||
"default": 0
|
||||
},
|
||||
],
|
||||
|
||||
|
@ -9,9 +9,18 @@ from typing import Optional, Set, Tuple
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
nowtime,
|
||||
parse_json,
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
@ -712,11 +721,10 @@ class update_entries_after(object):
|
||||
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and (
|
||||
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
|
||||
)
|
||||
and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
||||
and sle.voucher_detail_no
|
||||
and not self.args.get("sle_id")
|
||||
and sle.is_cancelled == 0
|
||||
):
|
||||
self.reset_actual_qty_for_stock_reco(sle)
|
||||
|
||||
@ -737,6 +745,23 @@ class update_entries_after(object):
|
||||
|
||||
if sle.serial_and_batch_bundle:
|
||||
self.calculate_valuation_for_serial_batch_bundle(sle)
|
||||
elif sle.serial_no and not self.args.get("sle_id"):
|
||||
# Only run in reposting
|
||||
self.get_serialized_values(sle)
|
||||
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
||||
|
||||
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
|
||||
self.wh_data.valuation_rate
|
||||
)
|
||||
elif (
|
||||
sle.batch_no
|
||||
and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
|
||||
and not self.args.get("sle_id")
|
||||
):
|
||||
# Only run in reposting
|
||||
self.update_batched_values(sle)
|
||||
else:
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
||||
# assert
|
||||
@ -782,6 +807,45 @@ class update_entries_after(object):
|
||||
):
|
||||
self.update_outgoing_rate_on_transaction(sle)
|
||||
|
||||
def get_serialized_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
actual_qty = flt(sle.actual_qty)
|
||||
serial_nos = cstr(sle.serial_no).split("\n")
|
||||
|
||||
if incoming_rate < 0:
|
||||
# wrong incoming rate
|
||||
incoming_rate = self.wh_data.valuation_rate
|
||||
|
||||
stock_value_change = 0
|
||||
if actual_qty > 0:
|
||||
stock_value_change = actual_qty * incoming_rate
|
||||
else:
|
||||
# In case of delivery/stock issue, get average purchase rate
|
||||
# of serial nos of current entry
|
||||
if not sle.is_cancelled:
|
||||
outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
|
||||
stock_value_change = -1 * outgoing_value
|
||||
else:
|
||||
stock_value_change = actual_qty * sle.outgoing_rate
|
||||
|
||||
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
|
||||
|
||||
if new_stock_qty > 0:
|
||||
new_stock_value = (
|
||||
self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
|
||||
) + stock_value_change
|
||||
if new_stock_value >= 0:
|
||||
# calculate new valuation rate only if stock value is positive
|
||||
# else it remains the same as that of previous entry
|
||||
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
|
||||
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_rate = self.check_if_allow_zero_valuation_rate(
|
||||
sle.voucher_type, sle.voucher_detail_no
|
||||
)
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def reset_actual_qty_for_stock_reco(self, sle):
|
||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||
@ -795,6 +859,36 @@ class update_entries_after(object):
|
||||
if abs(sle.actual_qty) == 0.0:
|
||||
sle.is_cancelled = 1
|
||||
|
||||
if sle.serial_and_batch_bundle and frappe.get_cached_value(
|
||||
"Item", sle.item_code, "has_serial_no"
|
||||
):
|
||||
self.update_serial_no_status(sle)
|
||||
|
||||
def update_serial_no_status(self, sle):
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos
|
||||
|
||||
serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
warehouse = None
|
||||
status = "Inactive"
|
||||
|
||||
if sle.actual_qty > 0:
|
||||
warehouse = sle.warehouse
|
||||
status = "Active"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(sn_table.status, status)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
)
|
||||
|
||||
query.run()
|
||||
|
||||
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
|
||||
@ -1171,11 +1265,12 @@ class update_entries_after(object):
|
||||
outgoing_rate = get_batch_incoming_rate(
|
||||
item_code=sle.item_code,
|
||||
warehouse=sle.warehouse,
|
||||
serial_and_batch_bundle=sle.serial_and_batch_bundle,
|
||||
batch_no=sle.batch_no,
|
||||
posting_date=sle.posting_date,
|
||||
posting_time=sle.posting_time,
|
||||
creation=sle.creation,
|
||||
)
|
||||
|
||||
if outgoing_rate is None:
|
||||
# This can *only* happen if qty available for the batch is zero.
|
||||
# in such case fall back various other rates.
|
||||
@ -1449,11 +1544,10 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
|
||||
|
||||
def get_batch_incoming_rate(
|
||||
item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
|
||||
item_code, warehouse, batch_no, posting_date, posting_time, creation=None
|
||||
):
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
|
||||
posting_date, posting_time
|
||||
@ -1464,28 +1558,13 @@ def get_batch_incoming_rate(
|
||||
== CombineDatetime(posting_date, posting_time)
|
||||
) & (sle.creation < creation)
|
||||
|
||||
batches = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
|
||||
)
|
||||
|
||||
batch_details = (
|
||||
frappe.qb.from_(sle)
|
||||
.inner_join(batch_ledger)
|
||||
.on(sle.serial_and_batch_bundle == batch_ledger.parent)
|
||||
.select(
|
||||
Sum(
|
||||
Case()
|
||||
.when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
|
||||
.else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
|
||||
).as_("batch_value"),
|
||||
Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
|
||||
"batch_qty"
|
||||
),
|
||||
)
|
||||
.select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
|
||||
.where(
|
||||
(sle.item_code == item_code)
|
||||
& (sle.warehouse == warehouse)
|
||||
& (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
|
||||
& (sle.batch_no == batch_no)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
|
Loading…
x
Reference in New Issue
Block a user