Merge branch 'develop' into refactor/report/bom-stock-calculated

This commit is contained in:
Sagar Sharma
2022-09-12 17:23:15 +05:30
committed by GitHub
16 changed files with 501 additions and 492 deletions

View File

@@ -53,15 +53,13 @@ class BankStatementImport(DataImport):
if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.background_jobs import is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
if not is_job_queued(self.name):
enqueue(
start_import,
queue="default",

View File

@@ -4,22 +4,20 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.doctype.account.account import merge_account
class LedgerMerge(Document):
def start_merge(self):
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
if not is_job_queued(self.name):
enqueue(
start_merge,
queue="default",

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
from frappe.utils.background_jobs import enqueue, is_job_queued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -207,14 +207,12 @@ class OpeningInvoiceCreationTool(Document):
if len(invoices) < 50:
return start_import(invoices)
else:
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
if not is_job_queued(self.name):
enqueue(
start_import,
queue="default",

View File

@@ -6,11 +6,10 @@ import json
import frappe
from frappe import _
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue
from frappe.utils.background_jobs import enqueue, is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive
@@ -467,7 +466,7 @@ def enqueue_job(job, **kwargs):
closing_entry = kwargs.get("closing_entry") or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
if not is_job_queued(job_name):
enqueue(
job,
**kwargs,
@@ -491,12 +490,6 @@ def check_scheduler_status():
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True
def safe_load_json(message):
try:
json_message = json.loads(message).get("message")

View File

@@ -489,7 +489,6 @@ def make_reverse_gl_entries(
).run(as_dict=1)
if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1)
create_payment_ledger_entry(
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
)

View File

@@ -784,7 +784,7 @@ class ReceivablePayableReport(object):
def add_customer_filters(
self,
):
self.customter = qb.DocType("Customer")
self.customer = qb.DocType("Customer")
if self.filters.get("customer_group"):
self.get_hierarchical_filters("Customer Group", "customer_group")
@@ -838,7 +838,7 @@ class ReceivablePayableReport(object):
customer = self.customer
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
self.qb_selection_filter.append(ple.party.isin(customers))
def add_accounting_dimensions_filters(self):
accounting_dimensions = get_accounting_dimensions(as_list=False)

View File

@@ -205,6 +205,10 @@ class AccountsController(TransactionBase):
def on_trash(self):
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
ple = frappe.qb.DocType("Payment Ledger Entry")
frappe.qb.from_(ple).delete().where(
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
).run()
frappe.db.sql(
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
)

View File

@@ -309,7 +309,11 @@ class BuyingController(SubcontractingController):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1),
d.precision("rate"),
)
if self.is_internal_transfer():
if rate != d.rate:

View File

@@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
Filters = frappe._dict
Row = frappe._dict
@@ -14,15 +15,50 @@ QueryArgs = Dict[str, str]
def execute(filters: Filters) -> Tuple[Columns, Data]:
filters = frappe._dict(filters or {})
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters: Filters) -> Data:
query_args = get_query_args(filters)
data = run_query(query_args)
wo = frappe.qb.DocType("Work Order")
se = frappe.qb.DocType("Stock Entry")
query = (
frappe.qb.from_(wo)
.inner_join(se)
.on(wo.name == se.work_order)
.select(
wo.name,
wo.status,
wo.production_item,
wo.qty,
wo.produced_qty,
wo.process_loss_qty,
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
Sum(se.total_incoming_value).as_("total_fg_value"),
Sum(se.total_outgoing_value).as_("total_rm_value"),
)
.where(
(wo.process_loss_qty > 0)
& (wo.company == filters.company)
& (se.docstatus == 1)
& (se.posting_date.between(filters.from_date, filters.to_date))
)
.groupby(se.work_order)
)
if "item" in filters:
query.where(wo.production_item == filters.item)
if "work_order" in filters:
query.where(wo.name == filters.work_order)
data = query.run(as_dict=True)
update_data_with_total_pl_value(data)
return data
@@ -67,54 +103,7 @@ def get_columns() -> Columns:
]
def get_query_args(filters: Filters) -> QueryArgs:
query_args = {}
query_args.update(filters)
query_args.update(get_filter_conditions(filters))
return query_args
def run_query(query_args: QueryArgs) -> Data:
return frappe.db.sql(
"""
SELECT
wo.name, wo.status, wo.production_item, wo.qty,
wo.produced_qty, wo.process_loss_qty,
(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
sum(se.total_incoming_value) as total_fg_value,
sum(se.total_outgoing_value) as total_rm_value
FROM
`tabWork Order` wo INNER JOIN `tabStock Entry` se
ON wo.name=se.work_order
WHERE
process_loss_qty > 0
AND wo.company = %(company)s
AND se.docstatus = 1
AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
{item_filter}
{work_order_filter}
GROUP BY
se.work_order
""".format(
**query_args
),
query_args,
as_dict=1,
)
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
def get_filter_conditions(filters: Filters) -> QueryArgs:
filter_conditions = dict(item_filter="", work_order_filter="")
if "item" in filters:
production_item = filters.get("item")
filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
if "work_order" in filters:
work_order_name = filters.get("work_order")
filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
return filter_conditions

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull
from frappe.utils import cint
@@ -17,70 +18,70 @@ def execute(filters=None):
def get_item_list(wo_list, filters):
out = []
# Add a row for each item/qty
for wo_details in wo_list:
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
if wo_list:
bin = frappe.qb.DocType("Bin")
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType("BOM Item")
for wo_item_details in frappe.db.get_values(
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
):
# Add a row for each item/qty
for wo_details in wo_list:
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
item_list = frappe.db.sql(
"""SELECT
bom_item.item_code as item_code,
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
FROM
`tabBOM` as bom, `tabBOM Item` AS bom_item
LEFT JOIN `tabBin` AS ledger
ON bom_item.item_code = ledger.item_code
AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s)
WHERE
bom.name = bom_item.parent
and bom_item.item_code = %(item_code)s
and bom.name = %(bom)s
GROUP BY
bom_item.item_code""",
{
"bom": wo_details.bom_no,
"warehouse": wo_item_details.source_warehouse,
"filterhouse": filters.warehouse,
"item_code": wo_item_details.item_code,
},
as_dict=1,
)
for wo_item_details in frappe.db.get_values(
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
):
item_list = (
frappe.qb.from_(bom)
.from_(bom_item)
.left_join(bin)
.on(
(bom_item.item_code == bin.item_code)
& (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
)
.select(
bom_item.item_code.as_("item_code"),
IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
)
.where(
(bom.name == bom_item.parent)
& (bom_item.item_code == wo_item_details.item_code)
& (bom.name == wo_details.bom_no)
)
.groupby(bom_item.item_code)
).run(as_dict=1)
stock_qty = 0
count = 0
buildable_qty = wo_details.qty
for item in item_list:
count = count + 1
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
stock_qty = stock_qty + 1
elif buildable_qty >= item.build_qty:
buildable_qty = item.build_qty
stock_qty = 0
count = 0
buildable_qty = wo_details.qty
for item in item_list:
count = count + 1
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
stock_qty = stock_qty + 1
elif buildable_qty >= item.build_qty:
buildable_qty = item.build_qty
if count == stock_qty:
build = "Y"
else:
build = "N"
if count == stock_qty:
build = "Y"
else:
build = "N"
row = frappe._dict(
{
"work_order": wo_details.name,
"status": wo_details.status,
"req_items": cint(count),
"instock": stock_qty,
"description": desc,
"source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no,
"qty": wo_details.qty,
"buildable_qty": buildable_qty,
"ready_to_build": build,
}
)
row = frappe._dict(
{
"work_order": wo_details.name,
"status": wo_details.status,
"req_items": cint(count),
"instock": stock_qty,
"description": desc,
"source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no,
"qty": wo_details.qty,
"buildable_qty": buildable_qty,
"ready_to_build": build,
}
)
out.append(row)
out.append(row)
return out

View File

@@ -795,7 +795,7 @@
},
{
"fieldname": "customer_code",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Customer Code",
"no_copy": 1,
@@ -910,7 +910,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-06-15 09:02:06.177691",
"modified": "2022-09-12 15:00:10.130340",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -778,6 +778,14 @@ class TestItem(FrappeTestCase):
item.has_batch_no = 1
item.save()
def test_customer_codes_length(self):
"""Check if item code with special characters are allowed."""
item = make_item(properties={"item_code": "Test Item Code With Special Characters"})
for row in range(3):
item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)})
item.save()
self.assertTrue(len(item.customer_code) > 140)
def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings")

View File

@@ -815,7 +815,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
return {
"filters": {
"docstatus": 1,
"company": me.frm.doc.company
"company": me.frm.doc.company,
"status": ["not in", ["Completed", "Closed"]]
}
};
});

View File

@@ -117,6 +117,7 @@ class StockEntry(StockController):
self.validate_work_order()
self.validate_bom()
self.validate_purchase_order()
self.validate_subcontracting_order()
if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items()
@@ -959,6 +960,20 @@ class StockEntry(StockController):
)
)
def validate_subcontracting_order(self):
if self.get("subcontracting_order") and self.purpose in [
"Send to Subcontractor",
"Material Transfer",
]:
sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status")
if sco_status == "Closed":
frappe.throw(
_("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format(
self.subcontracting_order
)
)
def mark_finished_and_scrap_items(self):
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return