mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-16 11:39:18 +00:00
Merge pull request #30804 from frappe/version-13-hotfix
chore: version 13 pre release
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
- name: Setup Node.js v14
|
- name: Setup Node.js v14
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
@@ -21,5 +22,10 @@ jobs:
|
|||||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
env:
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
GIT_AUTHOR_NAME: "Frappe PR Bot"
|
||||||
|
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||||
|
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||||
|
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||||
run: npx semantic-release
|
run: npx semantic-release
|
||||||
@@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
|||||||
if doctype == "Budget":
|
if doctype == "Budget":
|
||||||
add_dimension_to_budget_doctype(df.copy(), doc)
|
add_dimension_to_budget_doctype(df.copy(), doc)
|
||||||
else:
|
else:
|
||||||
create_custom_field(doctype, df)
|
create_custom_field(doctype, df, ignore_validate=True)
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
create_custom_field("Budget", df)
|
create_custom_field("Budget", df, ignore_validate=True)
|
||||||
|
|
||||||
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
|
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt, fmt_money, getdate, nowdate
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
|
from frappe.utils import flt, fmt_money, getdate
|
||||||
|
|
||||||
|
import erpnext
|
||||||
|
|
||||||
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
|
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
|
||||||
|
|
||||||
@@ -76,6 +79,52 @@ class BankClearance(Document):
|
|||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||||
|
|
||||||
|
loan_disbursements = (
|
||||||
|
frappe.qb.from_(loan_disbursement)
|
||||||
|
.select(
|
||||||
|
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||||
|
loan_disbursement.name.as_("payment_entry"),
|
||||||
|
loan_disbursement.disbursed_amount.as_("credit"),
|
||||||
|
ConstantColumn(0).as_("debit"),
|
||||||
|
loan_disbursement.reference_number.as_("cheque_number"),
|
||||||
|
loan_disbursement.reference_date.as_("cheque_date"),
|
||||||
|
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||||
|
loan_disbursement.applicant.as_("against_account"),
|
||||||
|
)
|
||||||
|
.where(loan_disbursement.docstatus == 1)
|
||||||
|
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||||
|
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||||
|
.where(loan_disbursement.clearance_date.isnull())
|
||||||
|
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||||
|
.orderby(loan_disbursement.disbursement_date)
|
||||||
|
.orderby(loan_disbursement.name, frappe.qb.desc)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
|
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||||
|
|
||||||
|
loan_repayments = (
|
||||||
|
frappe.qb.from_(loan_repayment)
|
||||||
|
.select(
|
||||||
|
ConstantColumn("Loan Repayment").as_("payment_document"),
|
||||||
|
loan_repayment.name.as_("payment_entry"),
|
||||||
|
loan_repayment.amount_paid.as_("debit"),
|
||||||
|
ConstantColumn(0).as_("credit"),
|
||||||
|
loan_repayment.reference_number.as_("cheque_number"),
|
||||||
|
loan_repayment.reference_date.as_("cheque_date"),
|
||||||
|
loan_repayment.applicant.as_("against_account"),
|
||||||
|
loan_repayment.posting_date,
|
||||||
|
)
|
||||||
|
.where(loan_repayment.docstatus == 1)
|
||||||
|
.where(loan_repayment.clearance_date.isnull())
|
||||||
|
.where(loan_repayment.posting_date >= self.from_date)
|
||||||
|
.where(loan_repayment.posting_date <= self.to_date)
|
||||||
|
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||||
|
.orderby(loan_repayment.posting_date)
|
||||||
|
.orderby(loan_repayment.name, frappe.qb.desc)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||||
if self.include_pos_transactions:
|
if self.include_pos_transactions:
|
||||||
pos_sales_invoices = frappe.db.sql(
|
pos_sales_invoices = frappe.db.sql(
|
||||||
@@ -114,20 +163,29 @@ class BankClearance(Document):
|
|||||||
|
|
||||||
entries = sorted(
|
entries = sorted(
|
||||||
list(payment_entries)
|
list(payment_entries)
|
||||||
+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
|
+ list(journal_entries)
|
||||||
key=lambda k: k["posting_date"] or getdate(nowdate()),
|
+ list(pos_sales_invoices)
|
||||||
|
+ list(pos_purchase_invoices)
|
||||||
|
+ list(loan_disbursements)
|
||||||
|
+ list(loan_repayments),
|
||||||
|
key=lambda k: getdate(k["posting_date"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set("payment_entries", [])
|
self.set("payment_entries", [])
|
||||||
self.total_amount = 0.0
|
self.total_amount = 0.0
|
||||||
|
default_currency = erpnext.get_default_currency()
|
||||||
|
|
||||||
for d in entries:
|
for d in entries:
|
||||||
row = self.append("payment_entries", {})
|
row = self.append("payment_entries", {})
|
||||||
|
|
||||||
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
|
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
|
||||||
|
|
||||||
|
if not d.get("account_currency"):
|
||||||
|
d.account_currency = default_currency
|
||||||
|
|
||||||
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
|
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
|
||||||
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
|
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
|
||||||
|
d.posting_date = getdate(d.posting_date)
|
||||||
|
|
||||||
d.pop("credit")
|
d.pop("credit")
|
||||||
d.pop("debit")
|
d.pop("debit")
|
||||||
|
|||||||
@@ -1,9 +1,96 @@
|
|||||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import add_months, getdate
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
|
from erpnext.loan_management.doctype.loan.test_loan import (
|
||||||
|
create_loan,
|
||||||
|
create_loan_accounts,
|
||||||
|
create_loan_type,
|
||||||
|
create_repayment_entry,
|
||||||
|
make_loan_disbursement_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestBankClearance(unittest.TestCase):
|
class TestBankClearance(unittest.TestCase):
|
||||||
pass
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
make_bank_account()
|
||||||
|
create_loan_accounts()
|
||||||
|
create_loan_masters()
|
||||||
|
add_transactions()
|
||||||
|
|
||||||
|
# Basic test case to test if bank clearance tool doesn't break
|
||||||
|
# Detailed test can be added later
|
||||||
|
def test_bank_clearance(self):
|
||||||
|
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||||
|
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||||
|
bank_clearance.from_date = add_months(getdate(), -1)
|
||||||
|
bank_clearance.to_date = getdate()
|
||||||
|
bank_clearance.get_payment_entries()
|
||||||
|
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
||||||
|
|
||||||
|
|
||||||
|
def make_bank_account():
|
||||||
|
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Account",
|
||||||
|
"account_type": "Bank",
|
||||||
|
"account_name": "_Test Bank Clearance",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"parent_account": "Bank Accounts - _TC",
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
|
def create_loan_masters():
|
||||||
|
create_loan_type(
|
||||||
|
"Clearance Loan",
|
||||||
|
2000000,
|
||||||
|
13.5,
|
||||||
|
25,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
"Cash",
|
||||||
|
"_Test Bank Clearance - _TC",
|
||||||
|
"_Test Bank Clearance - _TC",
|
||||||
|
"Loan Account - _TC",
|
||||||
|
"Interest Income Account - _TC",
|
||||||
|
"Penalty Income Account - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_transactions():
|
||||||
|
make_payment_entry()
|
||||||
|
make_loan()
|
||||||
|
|
||||||
|
|
||||||
|
def make_loan():
|
||||||
|
loan = create_loan(
|
||||||
|
"_Test Customer",
|
||||||
|
"Clearance Loan",
|
||||||
|
280000,
|
||||||
|
"Repay Over Number of Periods",
|
||||||
|
20,
|
||||||
|
applicant_type="Customer",
|
||||||
|
)
|
||||||
|
loan.submit()
|
||||||
|
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
|
||||||
|
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
|
||||||
|
repayment_entry.save()
|
||||||
|
repayment_entry.submit()
|
||||||
|
|
||||||
|
|
||||||
|
def make_payment_entry():
|
||||||
|
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
|
||||||
|
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
|
||||||
|
pe.reference_no = "Conrad Oct 18"
|
||||||
|
pe.reference_date = "2018-10-24"
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
|||||||
@@ -1317,7 +1317,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if (
|
if (
|
||||||
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
|
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
|
||||||
):
|
):
|
||||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
|
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||||
|
self.company, "Purchase Invoice", self.name
|
||||||
|
)
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
|
|||||||
@@ -1473,7 +1473,9 @@ class SalesInvoice(SellingController):
|
|||||||
and self.base_rounding_adjustment
|
and self.base_rounding_adjustment
|
||||||
and not self.is_internal_transfer()
|
and not self.is_internal_transfer()
|
||||||
):
|
):
|
||||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
|
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||||
|
self.company, "Sales Invoice", self.name
|
||||||
|
)
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
|
|||||||
@@ -2007,6 +2007,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||||
|
|
||||||
def test_rounding_adjustment_3(self):
|
def test_rounding_adjustment_3(self):
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||||
|
create_dimension,
|
||||||
|
disable_dimension,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_dimension()
|
||||||
|
|
||||||
si = create_sales_invoice(do_not_save=True)
|
si = create_sales_invoice(do_not_save=True)
|
||||||
si.items = []
|
si.items = []
|
||||||
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
||||||
@@ -2034,6 +2041,10 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
"included_in_print_rate": 1,
|
"included_in_print_rate": 1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
si.cost_center = "_Test Cost Center 2 - _TC"
|
||||||
|
si.location = "Block 1"
|
||||||
|
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
self.assertEqual(si.net_total, 4007.16)
|
self.assertEqual(si.net_total, 4007.16)
|
||||||
@@ -2069,6 +2080,18 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(debit_credit_diff, 0)
|
self.assertEqual(debit_credit_diff, 0)
|
||||||
|
|
||||||
|
round_off_gle = frappe.db.get_value(
|
||||||
|
"GL Entry",
|
||||||
|
{"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
|
||||||
|
["cost_center", "location"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||||
|
self.assertEqual(round_off_gle.location, "Block 1")
|
||||||
|
|
||||||
|
disable_dimension()
|
||||||
|
|
||||||
def test_sales_invoice_with_shipping_rule(self):
|
def test_sales_invoice_with_shipping_rule(self):
|
||||||
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
|
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ def round_off_debit_credit(gl_map):
|
|||||||
|
|
||||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||||
gl_map[0].company
|
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||||
)
|
)
|
||||||
round_off_account_exists = False
|
round_off_account_exists = False
|
||||||
round_off_gle = frappe._dict()
|
round_off_gle = frappe._dict()
|
||||||
@@ -310,14 +310,43 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
update_accounting_dimensions(round_off_gle)
|
||||||
|
|
||||||
if not round_off_account_exists:
|
if not round_off_account_exists:
|
||||||
gl_map.append(round_off_gle)
|
gl_map.append(round_off_gle)
|
||||||
|
|
||||||
|
|
||||||
def get_round_off_account_and_cost_center(company):
|
def update_accounting_dimensions(round_off_gle):
|
||||||
|
dimensions = get_accounting_dimensions()
|
||||||
|
meta = frappe.get_meta(round_off_gle["voucher_type"])
|
||||||
|
has_all_dimensions = True
|
||||||
|
|
||||||
|
for dimension in dimensions:
|
||||||
|
if not meta.has_field(dimension):
|
||||||
|
has_all_dimensions = False
|
||||||
|
|
||||||
|
if dimensions and has_all_dimensions:
|
||||||
|
dimension_values = frappe.db.get_value(
|
||||||
|
round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
for dimension in dimensions:
|
||||||
|
round_off_gle[dimension] = dimension_values.get(dimension)
|
||||||
|
|
||||||
|
|
||||||
|
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
|
||||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
||||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||||
) or [None, None]
|
) or [None, None]
|
||||||
|
|
||||||
|
meta = frappe.get_meta(voucher_type)
|
||||||
|
|
||||||
|
# Give first preference to parent cost center for round off GLE
|
||||||
|
if meta.has_field("cost_center"):
|
||||||
|
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
|
||||||
|
if parent_cost_center:
|
||||||
|
round_off_cost_center = parent_cost_center
|
||||||
|
|
||||||
if not round_off_account:
|
if not round_off_account:
|
||||||
frappe.throw(_("Please mention Round Off Account in Company"))
|
frappe.throw(_("Please mention Round Off Account in Company"))
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe.test_runner import make_test_objects
|
from frappe.test_runner import make_test_objects
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_shipping_address
|
from erpnext.accounts.party import get_party_shipping_address
|
||||||
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries
|
from erpnext.accounts.utils import (
|
||||||
|
get_future_stock_vouchers,
|
||||||
|
get_voucherwise_gl_entries,
|
||||||
|
sort_stock_vouchers_by_posting_date,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
@@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase):
|
|||||||
msg="get_voucherwise_gl_entries not returning expected GLes",
|
msg="get_voucherwise_gl_entries not returning expected GLes",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_stock_voucher_sorting(self):
|
||||||
|
vouchers = []
|
||||||
|
|
||||||
|
item = make_item().name
|
||||||
|
|
||||||
|
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
|
||||||
|
|
||||||
|
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
|
||||||
|
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
|
||||||
|
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
|
||||||
|
|
||||||
|
for doc in (se1, se2, se3):
|
||||||
|
vouchers.append((doc.doctype, doc.name))
|
||||||
|
|
||||||
|
vouchers.append(("Stock Entry", "Wat"))
|
||||||
|
|
||||||
|
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
|
||||||
|
self.assertEqual(sorted_vouchers, vouchers)
|
||||||
|
|
||||||
|
|
||||||
ADDRESS_RECORDS = [
|
ADDRESS_RECORDS = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from json import loads
|
from json import loads
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
@@ -1123,6 +1124,9 @@ def update_gl_entries_after(
|
|||||||
def repost_gle_for_stock_vouchers(
|
def repost_gle_for_stock_vouchers(
|
||||||
stock_vouchers, posting_date, company=None, warehouse_account=None
|
stock_vouchers, posting_date, company=None, warehouse_account=None
|
||||||
):
|
):
|
||||||
|
if not stock_vouchers:
|
||||||
|
return
|
||||||
|
|
||||||
def _delete_gl_entries(voucher_type, voucher_no):
|
def _delete_gl_entries(voucher_type, voucher_no):
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""delete from `tabGL Entry`
|
"""delete from `tabGL Entry`
|
||||||
@@ -1130,6 +1134,8 @@ def repost_gle_for_stock_vouchers(
|
|||||||
(voucher_type, voucher_no),
|
(voucher_type, voucher_no),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
||||||
|
|
||||||
if not warehouse_account:
|
if not warehouse_account:
|
||||||
warehouse_account = get_warehouse_account_map(company)
|
warehouse_account = get_warehouse_account_map(company)
|
||||||
|
|
||||||
@@ -1150,6 +1156,27 @@ def repost_gle_for_stock_vouchers(
|
|||||||
_delete_gl_entries(voucher_type, voucher_no)
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
|
|
||||||
|
|
||||||
|
def sort_stock_vouchers_by_posting_date(
|
||||||
|
stock_vouchers: List[Tuple[str, str]]
|
||||||
|
) -> List[Tuple[str, str]]:
|
||||||
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
voucher_nos = [v[1] for v in stock_vouchers]
|
||||||
|
|
||||||
|
sles = (
|
||||||
|
frappe.qb.from_(sle)
|
||||||
|
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
||||||
|
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
||||||
|
.groupby(sle.voucher_type, sle.voucher_no)
|
||||||
|
).run(as_dict=True)
|
||||||
|
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||||
|
|
||||||
|
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
|
||||||
|
if unknown_vouchers:
|
||||||
|
sorted_vouchers.extend(unknown_vouchers)
|
||||||
|
|
||||||
|
return sorted_vouchers
|
||||||
|
|
||||||
|
|
||||||
def get_future_stock_vouchers(
|
def get_future_stock_vouchers(
|
||||||
posting_date, posting_time, for_warehouses=None, for_items=None, company=None
|
posting_date, posting_time, for_warehouses=None, for_items=None, company=None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -628,7 +628,7 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
asset_value_after_full_schedule = flt(
|
asset_value_after_full_schedule = flt(
|
||||||
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
||||||
self.precision("gross_purchase_amount"),
|
row.precision("expected_value_after_useful_life"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ frappe.ui.form.on("E Commerce Settings", {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.model.with_doctype("Item", () => {
|
frappe.model.with_doctype("Website Item", () => {
|
||||||
const web_item_meta = frappe.get_meta('Website Item');
|
const web_item_meta = frappe.get_meta('Website Item');
|
||||||
|
|
||||||
const valid_fields = web_item_meta.fields.filter(
|
const valid_fields = web_item_meta.fields.filter(
|
||||||
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
|
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
|
||||||
).map(df => ({ label: df.label, value: df.fieldname }));
|
).map(df => ({ label: df.label, value: df.fieldname }));
|
||||||
|
|
||||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
frm.get_field("filter_fields").grid.update_docfield_property(
|
||||||
'fieldname', 'fieldtype', 'Select'
|
'fieldname', 'fieldtype', 'Select'
|
||||||
);
|
);
|
||||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
frm.get_field("filter_fields").grid.update_docfield_property(
|
||||||
'fieldname', 'options', valid_fields
|
'fieldname', 'options', valid_fields
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ECommerceSettings(Document):
|
|||||||
self.is_redisearch_loaded = is_search_module_loaded()
|
self.is_redisearch_loaded = is_search_module_loaded()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_field_filters()
|
self.validate_field_filters(self.filter_fields, self.enable_field_filters)
|
||||||
self.validate_attribute_filters()
|
self.validate_attribute_filters()
|
||||||
self.validate_checkout()
|
self.validate_checkout()
|
||||||
self.validate_search_index_fields()
|
self.validate_search_index_fields()
|
||||||
@@ -50,21 +50,22 @@ class ECommerceSettings(Document):
|
|||||||
define_autocomplete_dictionary()
|
define_autocomplete_dictionary()
|
||||||
create_website_items_index()
|
create_website_items_index()
|
||||||
|
|
||||||
def validate_field_filters(self):
|
@staticmethod
|
||||||
if not (self.enable_field_filters and self.filter_fields):
|
def validate_field_filters(filter_fields, enable_field_filters):
|
||||||
|
if not (enable_field_filters and filter_fields):
|
||||||
return
|
return
|
||||||
|
|
||||||
item_meta = frappe.get_meta("Item")
|
web_item_meta = frappe.get_meta("Website Item")
|
||||||
valid_fields = [
|
valid_fields = [
|
||||||
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||||
]
|
]
|
||||||
|
|
||||||
for f in self.filter_fields:
|
for row in filter_fields:
|
||||||
if f.fieldname not in valid_fields:
|
if row.fieldname not in valid_fields:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'"
|
"Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'"
|
||||||
).format(f.idx, f.fieldname)
|
).format(row.idx, frappe.bold(row.fieldname))
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_attribute_filters(self):
|
def validate_attribute_filters(self):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
@@ -11,42 +11,34 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
|||||||
|
|
||||||
|
|
||||||
class TestECommerceSettings(unittest.TestCase):
|
class TestECommerceSettings(unittest.TestCase):
|
||||||
def setUp(self):
|
def tearDown(self):
|
||||||
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
frappe.db.rollback()
|
||||||
|
|
||||||
def get_cart_settings(self):
|
|
||||||
return frappe.get_doc({"doctype": "E Commerce Settings", "company": "_Test Company"})
|
|
||||||
|
|
||||||
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
|
||||||
# We aren't checking just currency exchange record anymore
|
|
||||||
# while validating price list currency exchange rate to that of company.
|
|
||||||
# The API is being used to fetch the rate which again almost always
|
|
||||||
# gives back a valid value (for valid currencies).
|
|
||||||
# This makes the test obsolete.
|
|
||||||
# Commenting because im not sure if there's a better test we can write
|
|
||||||
|
|
||||||
# def test_exchange_rate_exists(self):
|
|
||||||
# frappe.db.sql("""delete from `tabCurrency Exchange`""")
|
|
||||||
|
|
||||||
# cart_settings = self.get_cart_settings()
|
|
||||||
# cart_settings.price_list = "_Test Price List Rest of the World"
|
|
||||||
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
|
|
||||||
|
|
||||||
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
|
|
||||||
# currency_exchange_records
|
|
||||||
# frappe.get_doc(currency_exchange_records[0]).insert()
|
|
||||||
# cart_settings.validate_price_list_exchange_rate()
|
|
||||||
|
|
||||||
def test_tax_rule_validation(self):
|
def test_tax_rule_validation(self):
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||||
|
|
||||||
cart_settings = self.get_cart_settings()
|
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||||
cart_settings.enabled = 1
|
cart_settings.enabled = 1
|
||||||
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
||||||
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
|
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
|
||||||
|
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
||||||
|
|
||||||
|
def test_invalid_filter_fields(self):
|
||||||
|
"Check if Item fields are blocked in E Commerce Settings filter fields."
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
|
|
||||||
|
setup_e_commerce_settings({"enable_field_filters": 1})
|
||||||
|
|
||||||
|
create_custom_field(
|
||||||
|
"Item",
|
||||||
|
dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"),
|
||||||
|
)
|
||||||
|
settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
settings.append("filter_fields", {"fieldname": "test_data"})
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, settings.save)
|
||||||
|
|
||||||
|
|
||||||
def setup_e_commerce_settings(values_dict):
|
def setup_e_commerce_settings(values_dict):
|
||||||
"Accepts a dict of values that updates E Commerce Settings."
|
"Accepts a dict of values that updates E Commerce Settings."
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ class ProductFiltersBuilder:
|
|||||||
fields, filter_data = [], []
|
fields, filter_data = [], []
|
||||||
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
|
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
|
||||||
|
|
||||||
# filter valid field filters i.e. those that exist in Item
|
# filter valid field filters i.e. those that exist in Website Item
|
||||||
item_meta = frappe.get_meta("Item", cached=True)
|
web_item_meta = frappe.get_meta("Website Item", cached=True)
|
||||||
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
|
fields = [
|
||||||
|
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
|
||||||
|
]
|
||||||
|
|
||||||
for df in fields:
|
for df in fields:
|
||||||
item_filters, item_or_filters = {"published_in_website": 1}, []
|
item_filters, item_or_filters = {"published": 1}, []
|
||||||
link_doctype_values = self.get_filtered_link_doctype_records(df)
|
link_doctype_values = self.get_filtered_link_doctype_records(df)
|
||||||
|
|
||||||
if df.fieldtype == "Link":
|
if df.fieldtype == "Link":
|
||||||
@@ -50,9 +52,13 @@ class ProductFiltersBuilder:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# exclude variants if mentioned in settings
|
||||||
|
if frappe.db.get_single_value("E Commerce Settings", "hide_variants"):
|
||||||
|
item_filters["variant_of"] = ["is", "not set"]
|
||||||
|
|
||||||
# Get link field values attached to published items
|
# Get link field values attached to published items
|
||||||
item_values = frappe.get_all(
|
item_values = frappe.get_all(
|
||||||
"Item",
|
"Website Item",
|
||||||
fields=[df.fieldname],
|
fields=[df.fieldname],
|
||||||
filters=item_filters,
|
filters=item_filters,
|
||||||
or_filters=item_or_filters,
|
or_filters=item_or_filters,
|
||||||
|
|||||||
@@ -277,6 +277,54 @@ class TestProductDataEngine(unittest.TestCase):
|
|||||||
# tear down
|
# tear down
|
||||||
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
|
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
|
||||||
|
|
||||||
|
def test_custom_field_as_filter(self):
|
||||||
|
"Test if custom field functions as filter correctly."
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
|
|
||||||
|
create_custom_field(
|
||||||
|
"Website Item",
|
||||||
|
dict(
|
||||||
|
owner="Administrator",
|
||||||
|
fieldname="supplier",
|
||||||
|
label="Supplier",
|
||||||
|
fieldtype="Link",
|
||||||
|
options="Supplier",
|
||||||
|
insert_after="on_backorder",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier"
|
||||||
|
)
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
settings.append("filter_fields", {"fieldname": "supplier"})
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
field_filters = filter_engine.get_field_filters()
|
||||||
|
custom_filter = field_filters[1]
|
||||||
|
filter_values = custom_filter[1]
|
||||||
|
|
||||||
|
self.assertEqual(custom_filter[0].options, "Supplier")
|
||||||
|
self.assertEqual(len(filter_values), 2)
|
||||||
|
self.assertIn("_Test Supplier", filter_values)
|
||||||
|
|
||||||
|
# test if custom filter works in query
|
||||||
|
field_filters = {"supplier": "_Test Supplier 1"}
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if only 'Raw Material' are fetched in the right order
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
|
||||||
|
|
||||||
|
|
||||||
def create_variant_web_item():
|
def create_variant_web_item():
|
||||||
"Create Variant and Template Website Items."
|
"Create Variant and Template Website Items."
|
||||||
|
|||||||
@@ -57,11 +57,10 @@ def execute(filters=None):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
leave_list = None
|
leave_types = None
|
||||||
if filters.summarized_view:
|
if filters.summarized_view:
|
||||||
leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True)
|
leave_types = frappe.get_all("Leave Type", pluck="name")
|
||||||
leave_list = [d[0] + ":Float:120" for d in leave_types]
|
columns.extend([leave_type + ":Float:120" for leave_type in leave_types])
|
||||||
columns.extend(leave_list)
|
|
||||||
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
|
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
|
||||||
|
|
||||||
if filters.group_by:
|
if filters.group_by:
|
||||||
@@ -81,13 +80,19 @@ def execute(filters=None):
|
|||||||
holiday_map,
|
holiday_map,
|
||||||
conditions,
|
conditions,
|
||||||
default_holiday_list,
|
default_holiday_list,
|
||||||
leave_list=leave_list,
|
leave_types=leave_types,
|
||||||
)
|
)
|
||||||
emp_att_map.update(emp_att_data)
|
emp_att_map.update(emp_att_data)
|
||||||
data += record
|
data += record
|
||||||
else:
|
else:
|
||||||
record, emp_att_map = add_data(
|
record, emp_att_map = add_data(
|
||||||
emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list
|
emp_map,
|
||||||
|
att_map,
|
||||||
|
filters,
|
||||||
|
holiday_map,
|
||||||
|
conditions,
|
||||||
|
default_holiday_list,
|
||||||
|
leave_types=leave_types,
|
||||||
)
|
)
|
||||||
data += record
|
data += record
|
||||||
|
|
||||||
@@ -104,12 +109,10 @@ def get_chart_data(emp_att_map, days):
|
|||||||
{"name": "Leave", "values": []},
|
{"name": "Leave", "values": []},
|
||||||
]
|
]
|
||||||
for idx, day in enumerate(days, start=0):
|
for idx, day in enumerate(days, start=0):
|
||||||
p = day.replace("::65", "")
|
|
||||||
labels.append(day.replace("::65", ""))
|
labels.append(day.replace("::65", ""))
|
||||||
total_absent_on_day = 0
|
total_absent_on_day = 0
|
||||||
total_leave_on_day = 0
|
total_leave_on_day = 0
|
||||||
total_present_on_day = 0
|
total_present_on_day = 0
|
||||||
total_holiday = 0
|
|
||||||
for emp in emp_att_map.keys():
|
for emp in emp_att_map.keys():
|
||||||
if emp_att_map[emp][idx]:
|
if emp_att_map[emp][idx]:
|
||||||
if emp_att_map[emp][idx] == "A":
|
if emp_att_map[emp][idx] == "A":
|
||||||
@@ -134,9 +137,8 @@ def get_chart_data(emp_att_map, days):
|
|||||||
|
|
||||||
|
|
||||||
def add_data(
|
def add_data(
|
||||||
employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None
|
employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_types=None
|
||||||
):
|
):
|
||||||
|
|
||||||
record = []
|
record = []
|
||||||
emp_att_map = {}
|
emp_att_map = {}
|
||||||
for emp in employee_map:
|
for emp in employee_map:
|
||||||
@@ -222,7 +224,7 @@ def add_data(
|
|||||||
else:
|
else:
|
||||||
leaves[d.leave_type] = d.count
|
leaves[d.leave_type] = d.count
|
||||||
|
|
||||||
for d in leave_list:
|
for d in leave_types:
|
||||||
if d in leaves:
|
if d in leaves:
|
||||||
row.append(leaves[d])
|
row.append(leaves[d])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -73,7 +73,18 @@ frappe.ui.form.on('Job Card', {
|
|||||||
if (frm.doc.docstatus == 0 && !frm.is_new() &&
|
if (frm.doc.docstatus == 0 && !frm.is_new() &&
|
||||||
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||||
frm.trigger("prepare_timer_buttons");
|
|
||||||
|
// if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
|
||||||
|
// and if stock mvt for WIP is required
|
||||||
|
if (frm.doc.work_order) {
|
||||||
|
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
|
||||||
|
if (result.skip_transfer === 1 || result.status == 'In Process') {
|
||||||
|
frm.trigger("prepare_timer_buttons");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frm.trigger("prepare_timer_buttons");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frm.trigger("setup_quality_inspection");
|
frm.trigger("setup_quality_inspection");
|
||||||
|
|
||||||
|
|||||||
@@ -359,3 +359,4 @@ erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
|||||||
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
|
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
|
||||||
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
||||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||||
|
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"Add Field Filters, that are not standard fields in Website Item, as Custom Fields."
|
||||||
|
|
||||||
|
def move_table_multiselect_data(docfield):
|
||||||
|
"Copy child table data (Table Multiselect) from Item to Website Item for a docfield."
|
||||||
|
table_multiselect_data = get_table_multiselect_data(docfield)
|
||||||
|
field = docfield.fieldname
|
||||||
|
|
||||||
|
for row in table_multiselect_data:
|
||||||
|
# add copied multiselect data rows in Website Item
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
|
||||||
|
web_item_doc = frappe.get_doc("Website Item", web_item)
|
||||||
|
|
||||||
|
child_doc = frappe.new_doc(docfield.options, web_item_doc, field)
|
||||||
|
|
||||||
|
for field in ["name", "creation", "modified", "idx"]:
|
||||||
|
row[field] = None
|
||||||
|
|
||||||
|
child_doc.update(row)
|
||||||
|
|
||||||
|
child_doc.parenttype = "Website Item"
|
||||||
|
child_doc.parent = web_item
|
||||||
|
|
||||||
|
child_doc.insert()
|
||||||
|
|
||||||
|
def get_table_multiselect_data(docfield):
|
||||||
|
child_table = frappe.qb.DocType(docfield.options)
|
||||||
|
item = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
table_multiselect_data = ( # query table data for field
|
||||||
|
frappe.qb.from_(child_table)
|
||||||
|
.join(item)
|
||||||
|
.on(item.item_code == child_table.parent)
|
||||||
|
.select(child_table.star)
|
||||||
|
.where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
return table_multiselect_data
|
||||||
|
|
||||||
|
settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
|
||||||
|
if not (settings.enable_field_filters or settings.filter_fields):
|
||||||
|
return
|
||||||
|
|
||||||
|
item_meta = frappe.get_meta("Item")
|
||||||
|
valid_item_fields = [
|
||||||
|
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||||
|
]
|
||||||
|
|
||||||
|
web_item_meta = frappe.get_meta("Website Item")
|
||||||
|
valid_web_item_fields = [
|
||||||
|
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||||
|
]
|
||||||
|
|
||||||
|
for row in settings.filter_fields:
|
||||||
|
# skip if illegal field
|
||||||
|
if row.fieldname not in valid_item_fields:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if Item field is not in Website Item, add it as a custom field
|
||||||
|
if row.fieldname not in valid_web_item_fields:
|
||||||
|
df = item_meta.get_field(row.fieldname)
|
||||||
|
create_custom_field(
|
||||||
|
"Website Item",
|
||||||
|
dict(
|
||||||
|
owner="Administrator",
|
||||||
|
fieldname=df.fieldname,
|
||||||
|
label=df.label,
|
||||||
|
fieldtype=df.fieldtype,
|
||||||
|
options=df.options,
|
||||||
|
description=df.description,
|
||||||
|
read_only=df.read_only,
|
||||||
|
no_copy=df.no_copy,
|
||||||
|
insert_after="on_backorder",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# map field values
|
||||||
|
if df.fieldtype == "Table MultiSelect":
|
||||||
|
move_table_multiselect_data(df)
|
||||||
|
else:
|
||||||
|
frappe.db.sql( # nosemgrep
|
||||||
|
"""
|
||||||
|
UPDATE `tabWebsite Item` wi, `tabItem` i
|
||||||
|
SET wi.{0} = i.{0}
|
||||||
|
WHERE wi.item_code = i.item_code
|
||||||
|
""".format(
|
||||||
|
row.fieldname
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ import traceback
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import jwt
|
import jwt
|
||||||
|
import requests
|
||||||
import six
|
import six
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||||
@@ -552,6 +553,7 @@ def validate_totals(einvoice):
|
|||||||
+ flt(value_details["CgstVal"])
|
+ flt(value_details["CgstVal"])
|
||||||
+ flt(value_details["SgstVal"])
|
+ flt(value_details["SgstVal"])
|
||||||
+ flt(value_details["IgstVal"])
|
+ flt(value_details["IgstVal"])
|
||||||
|
+ flt(value_details["CesVal"])
|
||||||
+ flt(value_details["OthChrg"])
|
+ flt(value_details["OthChrg"])
|
||||||
- flt(value_details["Discount"])
|
- flt(value_details["Discount"])
|
||||||
)
|
)
|
||||||
@@ -828,14 +830,25 @@ class GSPConnector:
|
|||||||
return self.e_invoice_settings.auth_token
|
return self.e_invoice_settings.auth_token
|
||||||
|
|
||||||
def make_request(self, request_type, url, headers=None, data=None):
|
def make_request(self, request_type, url, headers=None, data=None):
|
||||||
if request_type == "post":
|
try:
|
||||||
res = make_post_request(url, headers=headers, data=data)
|
if request_type == "post":
|
||||||
else:
|
res = make_post_request(url, headers=headers, data=data)
|
||||||
res = make_get_request(url, headers=headers, data=data)
|
else:
|
||||||
|
res = make_get_request(url, headers=headers, data=data)
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code in [401, 403] and not hasattr(self, "token_auto_refreshed"):
|
||||||
|
self.auto_refresh_token()
|
||||||
|
headers = self.get_headers()
|
||||||
|
return self.make_request(request_type, url, headers, data)
|
||||||
|
|
||||||
self.log_request(url, headers, data, res)
|
self.log_request(url, headers, data, res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def auto_refresh_token(self):
|
||||||
|
self.fetch_auth_token()
|
||||||
|
self.token_auto_refreshed = True
|
||||||
|
|
||||||
def log_request(self, url, headers, data, res):
|
def log_request(self, url, headers, data, res):
|
||||||
headers.update({"password": self.credentials.password})
|
headers.update({"password": self.credentials.password})
|
||||||
request_log = frappe.get_doc(
|
request_log = frappe.get_doc(
|
||||||
@@ -1073,7 +1086,7 @@ class GSPConnector:
|
|||||||
"Distance": cint(eway_bill_details.distance),
|
"Distance": cint(eway_bill_details.distance),
|
||||||
"TransMode": eway_bill_details.mode_of_transport,
|
"TransMode": eway_bill_details.mode_of_transport,
|
||||||
"TransId": eway_bill_details.gstin,
|
"TransId": eway_bill_details.gstin,
|
||||||
"TransName": eway_bill_details.transporter,
|
"TransName": eway_bill_details.name,
|
||||||
"TrnDocDt": eway_bill_details.document_date,
|
"TrnDocDt": eway_bill_details.document_date,
|
||||||
"TrnDocNo": eway_bill_details.document_name,
|
"TrnDocNo": eway_bill_details.document_name,
|
||||||
"VehNo": eway_bill_details.vehicle_no,
|
"VehNo": eway_bill_details.vehicle_no,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Quotation(SellingController):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_uom_is_integer("stock_uom", "qty")
|
self.validate_uom_is_integer("stock_uom", "qty")
|
||||||
self.validate_valid_till()
|
self.validate_valid_till()
|
||||||
|
self.validate_shopping_cart_items()
|
||||||
self.set_customer_name()
|
self.set_customer_name()
|
||||||
if self.items:
|
if self.items:
|
||||||
self.with_items = 1
|
self.with_items = 1
|
||||||
@@ -38,6 +39,26 @@ class Quotation(SellingController):
|
|||||||
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
||||||
frappe.throw(_("Valid till date cannot be before transaction date"))
|
frappe.throw(_("Valid till date cannot be before transaction date"))
|
||||||
|
|
||||||
|
def validate_shopping_cart_items(self):
|
||||||
|
if self.order_type != "Shopping Cart":
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code})
|
||||||
|
|
||||||
|
# If variant is unpublished but template is published: valid
|
||||||
|
template = frappe.get_cached_value("Item", item.item_code, "variant_of")
|
||||||
|
if template and not has_web_item:
|
||||||
|
has_web_item = frappe.db.exists("Website Item", {"item_code": template})
|
||||||
|
|
||||||
|
if not has_web_item:
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format(
|
||||||
|
item.idx, frappe.bold(item.item_code)
|
||||||
|
),
|
||||||
|
title=_("Unpublished Item"),
|
||||||
|
)
|
||||||
|
|
||||||
def has_sales_order(self):
|
def has_sales_order(self):
|
||||||
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})
|
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,15 @@ class TestQuotation(FrappeTestCase):
|
|||||||
quotation.submit()
|
quotation.submit()
|
||||||
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
|
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
|
||||||
|
|
||||||
|
def test_shopping_cart_without_website_item(self):
|
||||||
|
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
|
||||||
|
frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete()
|
||||||
|
|
||||||
|
quotation = frappe.copy_doc(test_records[0])
|
||||||
|
quotation.order_type = "Shopping Cart"
|
||||||
|
quotation.valid_till = getdate()
|
||||||
|
self.assertRaises(frappe.ValidationError, quotation.validate)
|
||||||
|
|
||||||
def test_create_quotation_with_margin(self):
|
def test_create_quotation_with_margin(self):
|
||||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_data(conditions, filters):
|
def get_data(conditions, filters):
|
||||||
|
# nosemgrep
|
||||||
data = frappe.db.sql(
|
data = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -65,6 +66,7 @@ def get_data(conditions, filters):
|
|||||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||||
soi.qty, soi.delivered_qty,
|
soi.qty, soi.delivered_qty,
|
||||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||||
|
IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
|
||||||
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||||
soi.base_amount as amount,
|
soi.base_amount as amount,
|
||||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||||
@@ -75,9 +77,13 @@ def get_data(conditions, filters):
|
|||||||
soi.description as description
|
soi.description as description
|
||||||
FROM
|
FROM
|
||||||
`tabSales Order` so,
|
`tabSales Order` so,
|
||||||
`tabSales Order Item` soi
|
(`tabSales Order Item` soi
|
||||||
LEFT JOIN `tabSales Invoice Item` sii
|
LEFT JOIN `tabSales Invoice Item` sii
|
||||||
ON sii.so_detail = soi.name and sii.docstatus = 1
|
ON sii.so_detail = soi.name and sii.docstatus = 1)
|
||||||
|
LEFT JOIN `tabDelivery Note Item` dni
|
||||||
|
on dni.so_detail = soi.name
|
||||||
|
LEFT JOIN `tabDelivery Note` dn
|
||||||
|
on dni.parent = dn.name and dn.docstatus = 1
|
||||||
WHERE
|
WHERE
|
||||||
soi.parent = so.name
|
soi.parent = so.name
|
||||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||||
@@ -264,6 +270,12 @@ def get_columns(filters):
|
|||||||
},
|
},
|
||||||
{"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120},
|
{"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120},
|
||||||
{"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100},
|
{"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100},
|
||||||
|
{
|
||||||
|
"label": _("Time Taken to Deliver"),
|
||||||
|
"fieldname": "time_taken_to_deliver",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if not filters.get("group_by_so"):
|
if not filters.get("group_by_so"):
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days
|
||||||
|
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
from erpnext.selling.report.sales_order_analysis.sales_order_analysis import execute
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
|
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestSalesOrderAnalysis(FrappeTestCase):
|
||||||
|
def create_sales_order(self, transaction_date):
|
||||||
|
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||||
|
so = make_sales_order(
|
||||||
|
transaction_date=transaction_date,
|
||||||
|
item=item.item_code,
|
||||||
|
qty=10,
|
||||||
|
rate=100000,
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
so.po_no = ""
|
||||||
|
so.taxes_and_charges = ""
|
||||||
|
so.taxes = ""
|
||||||
|
so.items[0].delivery_date = add_days(transaction_date, 15)
|
||||||
|
so.save()
|
||||||
|
so.submit()
|
||||||
|
return item, so
|
||||||
|
|
||||||
|
def create_sales_invoice(self, so):
|
||||||
|
sinv = make_sales_invoice(so.name)
|
||||||
|
sinv.posting_date = so.transaction_date
|
||||||
|
sinv.taxes_and_charges = ""
|
||||||
|
sinv.taxes = ""
|
||||||
|
sinv.insert()
|
||||||
|
sinv.submit()
|
||||||
|
return sinv
|
||||||
|
|
||||||
|
def create_delivery_note(self, so):
|
||||||
|
dn = make_delivery_note(so.name)
|
||||||
|
dn.set_posting_time = True
|
||||||
|
dn.posting_date = add_days(so.transaction_date, 1)
|
||||||
|
dn.save()
|
||||||
|
dn.submit()
|
||||||
|
return dn
|
||||||
|
|
||||||
|
def test_01_so_to_deliver_and_bill(self):
|
||||||
|
transaction_date = "2021-06-01"
|
||||||
|
item, so = self.create_sales_order(transaction_date)
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
"status": ["To Deliver and Bill"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_value = {
|
||||||
|
"status": "To Deliver and Bill",
|
||||||
|
"sales_order": so.name,
|
||||||
|
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||||
|
"qty": 10,
|
||||||
|
"delivered_qty": 0,
|
||||||
|
"pending_qty": 10,
|
||||||
|
"qty_to_bill": 10,
|
||||||
|
"time_taken_to_deliver": 0,
|
||||||
|
}
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
for key, val in expected_value.items():
|
||||||
|
with self.subTest(key=key, val=val):
|
||||||
|
self.assertEqual(data[0][key], val)
|
||||||
|
|
||||||
|
def test_02_so_to_deliver(self):
|
||||||
|
transaction_date = "2021-06-01"
|
||||||
|
item, so = self.create_sales_order(transaction_date)
|
||||||
|
self.create_sales_invoice(so)
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
"status": ["To Deliver"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_value = {
|
||||||
|
"status": "To Deliver",
|
||||||
|
"sales_order": so.name,
|
||||||
|
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||||
|
"qty": 10,
|
||||||
|
"delivered_qty": 0,
|
||||||
|
"pending_qty": 10,
|
||||||
|
"qty_to_bill": 0,
|
||||||
|
"time_taken_to_deliver": 0,
|
||||||
|
}
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
for key, val in expected_value.items():
|
||||||
|
with self.subTest(key=key, val=val):
|
||||||
|
self.assertEqual(data[0][key], val)
|
||||||
|
|
||||||
|
def test_03_so_to_bill(self):
|
||||||
|
transaction_date = "2021-06-01"
|
||||||
|
item, so = self.create_sales_order(transaction_date)
|
||||||
|
self.create_delivery_note(so)
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
"status": ["To Bill"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_value = {
|
||||||
|
"status": "To Bill",
|
||||||
|
"sales_order": so.name,
|
||||||
|
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||||
|
"qty": 10,
|
||||||
|
"delivered_qty": 10,
|
||||||
|
"pending_qty": 0,
|
||||||
|
"qty_to_bill": 10,
|
||||||
|
"time_taken_to_deliver": 86400,
|
||||||
|
}
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
for key, val in expected_value.items():
|
||||||
|
with self.subTest(key=key, val=val):
|
||||||
|
self.assertEqual(data[0][key], val)
|
||||||
|
|
||||||
|
def test_04_so_completed(self):
|
||||||
|
transaction_date = "2021-06-01"
|
||||||
|
item, so = self.create_sales_order(transaction_date)
|
||||||
|
self.create_sales_invoice(so)
|
||||||
|
self.create_delivery_note(so)
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
"status": ["Completed"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_value = {
|
||||||
|
"status": "Completed",
|
||||||
|
"sales_order": so.name,
|
||||||
|
"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
|
||||||
|
"qty": 10,
|
||||||
|
"delivered_qty": 10,
|
||||||
|
"pending_qty": 0,
|
||||||
|
"qty_to_bill": 0,
|
||||||
|
"billed_qty": 10,
|
||||||
|
"time_taken_to_deliver": 86400,
|
||||||
|
}
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
for key, val in expected_value.items():
|
||||||
|
with self.subTest(key=key, val=val):
|
||||||
|
self.assertEqual(data[0][key], val)
|
||||||
|
|
||||||
|
def test_05_all_so_status(self):
|
||||||
|
columns, data, message, chart = execute(
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2021-06-01",
|
||||||
|
"to_date": "2021-06-30",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# SO's from first 4 test cases should be in output
|
||||||
|
self.assertEqual(len(data), 4)
|
||||||
@@ -72,17 +72,17 @@ frappe.ui.form.on("Item Group", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.model.with_doctype('Item', () => {
|
frappe.model.with_doctype('Website Item', () => {
|
||||||
const item_meta = frappe.get_meta('Item');
|
const web_item_meta = frappe.get_meta('Website Item');
|
||||||
|
|
||||||
const valid_fields = item_meta.fields.filter(
|
const valid_fields = web_item_meta.fields.filter(
|
||||||
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
|
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
|
||||||
).map(df => ({ label: df.label, value: df.fieldname }));
|
).map(df => ({ label: df.label, value: df.fieldname }));
|
||||||
|
|
||||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
frm.get_field("filter_fields").grid.update_docfield_property(
|
||||||
'fieldname', 'fieldtype', 'Select'
|
'fieldname', 'fieldtype', 'Select'
|
||||||
);
|
);
|
||||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
frm.get_field("filter_fields").grid.update_docfield_property(
|
||||||
'fieldname', 'options', valid_fields
|
'fieldname', 'options', valid_fields
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from frappe.website.render import clear_cache
|
|||||||
from frappe.website.website_generator import WebsiteGenerator
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
from six.moves.urllib.parse import quote
|
from six.moves.urllib.parse import quote
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings
|
||||||
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
|||||||
|
|
||||||
self.make_route()
|
self.make_route()
|
||||||
self.validate_item_group_defaults()
|
self.validate_item_group_defaults()
|
||||||
|
ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
NestedSet.on_update(self)
|
NestedSet.on_update(self)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder import Order
|
||||||
|
from frappe.query_builder.functions import CombineDatetime
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
|
||||||
@@ -134,24 +136,23 @@ def update_qty(bin_name, args):
|
|||||||
|
|
||||||
bin_details = get_bin_details(bin_name)
|
bin_details = get_bin_details(bin_name)
|
||||||
# actual qty is already updated by processing current voucher
|
# actual qty is already updated by processing current voucher
|
||||||
actual_qty = bin_details.actual_qty
|
actual_qty = bin_details.actual_qty or 0.0
|
||||||
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
# actual qty is not up to date in case of backdated transaction
|
# actual qty is not up to date in case of backdated transaction
|
||||||
if future_sle_exists(args):
|
if future_sle_exists(args):
|
||||||
actual_qty = (
|
last_sle_qty = (
|
||||||
frappe.db.get_value(
|
frappe.qb.from_(sle)
|
||||||
"Stock Ledger Entry",
|
.select(sle.qty_after_transaction)
|
||||||
filters={
|
.where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse")))
|
||||||
"item_code": args.get("item_code"),
|
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
|
||||||
"warehouse": args.get("warehouse"),
|
.orderby(sle.creation, order=Order.desc)
|
||||||
"is_cancelled": 0,
|
.run()
|
||||||
},
|
|
||||||
fieldname="qty_after_transaction",
|
|
||||||
order_by="posting_date desc, posting_time desc, creation desc",
|
|
||||||
)
|
|
||||||
or 0.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if last_sle_qty:
|
||||||
|
actual_qty = last_sle_qty[0][0]
|
||||||
|
|
||||||
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
||||||
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
|
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
|
||||||
indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty"))
|
indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty"))
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"error_section",
|
"error_section",
|
||||||
"error_log",
|
"error_log",
|
||||||
"items_to_be_repost",
|
"items_to_be_repost",
|
||||||
|
"affected_transactions",
|
||||||
"distinct_item_and_warehouse",
|
"distinct_item_and_warehouse",
|
||||||
"current_index"
|
"current_index"
|
||||||
],
|
],
|
||||||
@@ -172,12 +173,20 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "affected_transactions",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Affected Transactions",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-18 10:57:33.450907",
|
"modified": "2022-04-18 14:08:08.821602",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Repost Item Valuation",
|
"name": "Repost Item Valuation",
|
||||||
@@ -229,4 +238,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
|
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
|
||||||
from frappe.utils.user import get_users_with_role
|
from frappe.utils.user import get_users_with_role
|
||||||
from rq.timeouts import JobTimeoutException
|
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.utils import update_gl_entries_after
|
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
|
||||||
from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle
|
from erpnext.stock.stock_ledger import (
|
||||||
|
get_affected_transactions,
|
||||||
|
get_items_to_be_repost,
|
||||||
|
repost_future_sle,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RepostItemValuation(Document):
|
class RepostItemValuation(Document):
|
||||||
@@ -129,12 +132,12 @@ def repost(doc):
|
|||||||
|
|
||||||
doc.set_status("Completed")
|
doc.set_status("Completed")
|
||||||
|
|
||||||
except (Exception, JobTimeoutException):
|
except Exception:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
traceback = frappe.get_traceback()
|
traceback = frappe.get_traceback()
|
||||||
frappe.log_error(traceback)
|
frappe.log_error(traceback)
|
||||||
|
|
||||||
message = frappe.message_log.pop()
|
message = frappe.message_log.pop() if frappe.message_log else ""
|
||||||
if traceback:
|
if traceback:
|
||||||
message += "<br>" + "Traceback: <br>" + traceback
|
message += "<br>" + "Traceback: <br>" + traceback
|
||||||
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
||||||
@@ -170,6 +173,7 @@ def repost_sl_entries(doc):
|
|||||||
],
|
],
|
||||||
allow_negative_stock=doc.allow_negative_stock,
|
allow_negative_stock=doc.allow_negative_stock,
|
||||||
via_landed_cost_voucher=doc.via_landed_cost_voucher,
|
via_landed_cost_voucher=doc.via_landed_cost_voucher,
|
||||||
|
doc=doc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -177,27 +181,46 @@ def repost_gl_entries(doc):
|
|||||||
if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
|
if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# directly modified transactions
|
||||||
|
directly_dependent_transactions = _get_directly_dependent_vouchers(doc)
|
||||||
|
repost_affected_transaction = get_affected_transactions(doc)
|
||||||
|
repost_gle_for_stock_vouchers(
|
||||||
|
directly_dependent_transactions + list(repost_affected_transaction),
|
||||||
|
doc.posting_date,
|
||||||
|
doc.company,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_directly_dependent_vouchers(doc):
|
||||||
|
"""Get stock vouchers that are directly affected by reposting
|
||||||
|
i.e. any one item-warehouse is present in the stock transaction"""
|
||||||
|
|
||||||
|
items = set()
|
||||||
|
warehouses = set()
|
||||||
|
|
||||||
if doc.based_on == "Transaction":
|
if doc.based_on == "Transaction":
|
||||||
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
|
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
|
||||||
doc_items, doc_warehouses = ref_doc.get_items_and_warehouses()
|
doc_items, doc_warehouses = ref_doc.get_items_and_warehouses()
|
||||||
|
items.update(doc_items)
|
||||||
|
warehouses.update(doc_warehouses)
|
||||||
|
|
||||||
sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no)
|
sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no)
|
||||||
sle_items = [sle.item_code for sle in sles]
|
sle_items = {sle.item_code for sle in sles}
|
||||||
sle_warehouse = [sle.warehouse for sle in sles]
|
sle_warehouses = {sle.warehouse for sle in sles}
|
||||||
|
items.update(sle_items)
|
||||||
items = list(set(doc_items).union(set(sle_items)))
|
warehouses.update(sle_warehouses)
|
||||||
warehouses = list(set(doc_warehouses).union(set(sle_warehouse)))
|
|
||||||
else:
|
else:
|
||||||
items = [doc.item_code]
|
items.add(doc.item_code)
|
||||||
warehouses = [doc.warehouse]
|
warehouses.add(doc.warehouse)
|
||||||
|
|
||||||
update_gl_entries_after(
|
affected_vouchers = get_future_stock_vouchers(
|
||||||
doc.posting_date,
|
posting_date=doc.posting_date,
|
||||||
doc.posting_time,
|
posting_time=doc.posting_time,
|
||||||
for_warehouses=warehouses,
|
for_warehouses=list(warehouses),
|
||||||
for_items=items,
|
for_items=list(items),
|
||||||
company=doc.company,
|
company=doc.company,
|
||||||
)
|
)
|
||||||
|
return affected_vouchers
|
||||||
|
|
||||||
|
|
||||||
def notify_error_to_stock_managers(doc, traceback):
|
def notify_error_to_stock_managers(doc, traceback):
|
||||||
|
|||||||
@@ -186,3 +186,10 @@ class TestRepostItemValuation(FrappeTestCase):
|
|||||||
riv.db_set("status", "Skipped")
|
riv.db_set("status", "Skipped")
|
||||||
riv.reload()
|
riv.reload()
|
||||||
riv.cancel() # it should cancel now
|
riv.cancel() # it should cancel now
|
||||||
|
|
||||||
|
def test_queue_progress_serialization(self):
|
||||||
|
# Make sure set/tuple -> list behaviour is retained.
|
||||||
|
self.assertEqual(
|
||||||
|
[["a", "b"], ["c", "d"]],
|
||||||
|
sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))),
|
||||||
|
)
|
||||||
|
|||||||
@@ -802,11 +802,11 @@ def auto_fetch_serial_number(
|
|||||||
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
|
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
|
||||||
|
|
||||||
if batch_nos:
|
if batch_nos:
|
||||||
batch_nos = safe_json_loads(batch_nos)
|
batch_nos_list = safe_json_loads(batch_nos)
|
||||||
if isinstance(batch_nos, list):
|
if isinstance(batch_nos_list, list):
|
||||||
filters.batch_no = batch_nos
|
filters.batch_no = batch_nos_list
|
||||||
else:
|
else:
|
||||||
filters.batch_no = [str(batch_nos)]
|
filters.batch_no = [batch_nos]
|
||||||
|
|
||||||
if posting_date:
|
if posting_date:
|
||||||
filters.expiry_date = posting_date
|
filters.expiry_date = posting_date
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from frappe.core.page.permission_manager.permission_manager import reset
|
|||||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, today
|
from frappe.utils import add_days, today
|
||||||
|
from frappe.utils.data import add_to_date
|
||||||
|
|
||||||
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
||||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
@@ -624,6 +625,64 @@ class TestStockLedgerEntry(FrappeTestCase):
|
|||||||
receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15)
|
receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15)
|
||||||
self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}])
|
self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}])
|
||||||
|
|
||||||
|
def test_dependent_gl_entry_reposting(self):
|
||||||
|
def _get_stock_credit(doc):
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"GL Entry",
|
||||||
|
{
|
||||||
|
"voucher_no": doc.name,
|
||||||
|
"voucher_type": doc.doctype,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
"account": "Stock In Hand - TCP1",
|
||||||
|
},
|
||||||
|
"sum(credit)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _day(days):
|
||||||
|
return add_to_date(date=today(), days=days)
|
||||||
|
|
||||||
|
item = make_item().name
|
||||||
|
A = "Stores - TCP1"
|
||||||
|
B = "Work In Progress - TCP1"
|
||||||
|
C = "Finished Goods - TCP1"
|
||||||
|
|
||||||
|
make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=10, posting_date=_day(0))
|
||||||
|
make_stock_entry(item_code=item, from_warehouse=A, to_warehouse=B, qty=5, posting_date=_day(1))
|
||||||
|
depdendent_consumption = make_stock_entry(
|
||||||
|
item_code=item, from_warehouse=B, qty=5, posting_date=_day(2)
|
||||||
|
)
|
||||||
|
self.assertEqual(50, _get_stock_credit(depdendent_consumption))
|
||||||
|
|
||||||
|
# backdated receipt - should trigger GL repost of all previous stock entries
|
||||||
|
bd_receipt = make_stock_entry(
|
||||||
|
item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1)
|
||||||
|
)
|
||||||
|
self.assertEqual(100, _get_stock_credit(depdendent_consumption))
|
||||||
|
|
||||||
|
# cancelling receipt should reset it back
|
||||||
|
bd_receipt.cancel()
|
||||||
|
self.assertEqual(50, _get_stock_credit(depdendent_consumption))
|
||||||
|
|
||||||
|
bd_receipt2 = make_stock_entry(
|
||||||
|
item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2)
|
||||||
|
)
|
||||||
|
# total as per FIFO -> 2 * 20 + 3 * 10 = 70
|
||||||
|
self.assertEqual(70, _get_stock_credit(depdendent_consumption))
|
||||||
|
|
||||||
|
# transfer WIP material to final destination and consume it all
|
||||||
|
depdendent_consumption.cancel()
|
||||||
|
make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3))
|
||||||
|
final_consumption = make_stock_entry(
|
||||||
|
item_code=item, from_warehouse=C, qty=5, posting_date=_day(4)
|
||||||
|
)
|
||||||
|
# exact amount gets consumed
|
||||||
|
self.assertEqual(70, _get_stock_credit(final_consumption))
|
||||||
|
|
||||||
|
# cancel original backdated receipt - should repost A -> B -> C
|
||||||
|
bd_receipt2.cancel()
|
||||||
|
# original amount
|
||||||
|
self.assertEqual(50, _get_stock_credit(final_consumption))
|
||||||
|
|
||||||
|
|
||||||
def create_repack_entry(**args):
|
def create_repack_entry(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
|
|||||||
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
|
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_stock_and_account_balance
|
from erpnext.accounts.utils import get_stock_and_account_balance
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||||
@@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.flags.dont_execute_stock_reposts = None
|
frappe.flags.dont_execute_stock_reposts = None
|
||||||
|
frappe.local.future_sle = {}
|
||||||
|
|
||||||
def test_reco_for_fifo(self):
|
def test_reco_for_fifo(self):
|
||||||
self._test_reco_sle_gle("FIFO")
|
self._test_reco_sle_gle("FIFO")
|
||||||
@@ -310,9 +311,8 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
|
SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
|
||||||
PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
|
PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
|
||||||
"""
|
"""
|
||||||
item_code = "Backdated-Reco-Item"
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
create_item(item_code)
|
|
||||||
|
|
||||||
pr1 = make_purchase_receipt(
|
pr1 = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
||||||
@@ -394,9 +394,8 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
from erpnext.stock.stock_ledger import NegativeStockError
|
from erpnext.stock.stock_ledger import NegativeStockError
|
||||||
|
|
||||||
item_code = "Backdated-Reco-Item"
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
create_item(item_code)
|
|
||||||
|
|
||||||
pr1 = make_purchase_receipt(
|
pr1 = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2)
|
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2)
|
||||||
@@ -443,9 +442,8 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
from erpnext.stock.stock_ledger import NegativeStockError
|
from erpnext.stock.stock_ledger import NegativeStockError
|
||||||
|
|
||||||
item_code = "Backdated-Reco-Cancellation-Item"
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
create_item(item_code)
|
|
||||||
|
|
||||||
sr = create_stock_reconciliation(
|
sr = create_stock_reconciliation(
|
||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
@@ -486,9 +484,8 @@ class TestStockReconciliation(FrappeTestCase):
|
|||||||
frappe.flags.dont_execute_stock_reposts = True
|
frappe.flags.dont_execute_stock_reposts = True
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
item_code = "Backdated-Reco-Cancellation-Item"
|
item_code = make_item().name
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
create_item(item_code)
|
|
||||||
|
|
||||||
sr = create_stock_reconciliation(
|
sr = create_stock_reconciliation(
|
||||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10)
|
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
from typing import Set, Tuple
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -211,6 +212,7 @@ def repost_future_sle(
|
|||||||
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
|
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
|
||||||
|
|
||||||
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
|
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
|
||||||
|
affected_transactions = get_affected_transactions(doc)
|
||||||
|
|
||||||
i = get_current_index(doc) or 0
|
i = get_current_index(doc) or 0
|
||||||
while i < len(args):
|
while i < len(args):
|
||||||
@@ -226,6 +228,7 @@ def repost_future_sle(
|
|||||||
allow_negative_stock=allow_negative_stock,
|
allow_negative_stock=allow_negative_stock,
|
||||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||||
)
|
)
|
||||||
|
affected_transactions.update(obj.affected_transactions)
|
||||||
|
|
||||||
distinct_item_warehouses[
|
distinct_item_warehouses[
|
||||||
(args[i].get("item_code"), args[i].get("warehouse"))
|
(args[i].get("item_code"), args[i].get("warehouse"))
|
||||||
@@ -245,26 +248,32 @@ def repost_future_sle(
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
if doc and i % 2 == 0:
|
if doc and i % 2 == 0:
|
||||||
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
|
update_args_in_repost_item_valuation(
|
||||||
|
doc, i, args, distinct_item_warehouses, affected_transactions
|
||||||
|
)
|
||||||
|
|
||||||
if doc and args:
|
if doc and args:
|
||||||
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
|
update_args_in_repost_item_valuation(
|
||||||
|
doc, i, args, distinct_item_warehouses, affected_transactions
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
|
def update_args_in_repost_item_valuation(
|
||||||
frappe.db.set_value(
|
doc, index, args, distinct_item_warehouses, affected_transactions
|
||||||
doc.doctype,
|
):
|
||||||
doc.name,
|
doc.db_set(
|
||||||
{
|
{
|
||||||
"items_to_be_repost": json.dumps(args, default=str),
|
"items_to_be_repost": json.dumps(args, default=str),
|
||||||
"distinct_item_and_warehouse": json.dumps(
|
"distinct_item_and_warehouse": json.dumps(
|
||||||
{str(k): v for k, v in distinct_item_warehouses.items()}, default=str
|
{str(k): v for k, v in distinct_item_warehouses.items()}, default=str
|
||||||
),
|
),
|
||||||
"current_index": index,
|
"current_index": index,
|
||||||
},
|
"affected_transactions": frappe.as_json(affected_transactions),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.db.commit()
|
if not frappe.flags.in_test:
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
frappe.publish_realtime(
|
frappe.publish_realtime(
|
||||||
"item_reposting_progress",
|
"item_reposting_progress",
|
||||||
@@ -301,6 +310,14 @@ def get_distinct_item_warehouse(args=None, doc=None):
|
|||||||
return distinct_item_warehouses
|
return distinct_item_warehouses
|
||||||
|
|
||||||
|
|
||||||
|
def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
|
||||||
|
if not doc.affected_transactions:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
transactions = frappe.parse_json(doc.affected_transactions)
|
||||||
|
return {tuple(transaction) for transaction in transactions}
|
||||||
|
|
||||||
|
|
||||||
def get_current_index(doc=None):
|
def get_current_index(doc=None):
|
||||||
if doc and doc.current_index:
|
if doc and doc.current_index:
|
||||||
return doc.current_index
|
return doc.current_index
|
||||||
@@ -348,6 +365,7 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
self.new_items_found = False
|
self.new_items_found = False
|
||||||
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
|
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
|
||||||
|
self.affected_transactions: Set[Tuple[str, str]] = set()
|
||||||
|
|
||||||
self.data = frappe._dict()
|
self.data = frappe._dict()
|
||||||
self.initialize_previous_data(self.args)
|
self.initialize_previous_data(self.args)
|
||||||
@@ -506,6 +524,7 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
# previous sle data for this warehouse
|
# previous sle data for this warehouse
|
||||||
self.wh_data = self.data[sle.warehouse]
|
self.wh_data = self.data[sle.warehouse]
|
||||||
|
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
|
||||||
|
|
||||||
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
|
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
|
||||||
# validate negative stock for serialized items, fifo valuation
|
# validate negative stock for serialized items, fifo valuation
|
||||||
|
|||||||
@@ -9215,7 +9215,7 @@ Customer/Lead Name,Name des Kunden / Lead,
|
|||||||
Unmarked Days,Nicht markierte Tage,
|
Unmarked Days,Nicht markierte Tage,
|
||||||
Jan,Jan.,
|
Jan,Jan.,
|
||||||
Feb,Feb.,
|
Feb,Feb.,
|
||||||
Mar,Beschädigen,
|
Mar,Mrz.,
|
||||||
Apr,Apr.,
|
Apr,Apr.,
|
||||||
Aug,Aug.,
|
Aug,Aug.,
|
||||||
Sep,Sep.,
|
Sep,Sep.,
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user